Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MS custom idp & more custom execution steps #29

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package tech.neon.custom;

import java.util.stream.Stream;

import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SubjectCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.sessions.AuthenticationSessionModel;

public class NeonCleanUnverifiedAuthenticator extends AbstractIdpAuthenticator {
private static Logger logger = Logger.getLogger(NeonCleanUnverifiedAuthenticator.class);
@Override
public boolean requiresUser() {
return false;
}

@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
}

@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx,
BrokeredIdentityContext brokerContext) {
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
AuthenticationSessionModel authSession = context.getAuthenticationSession();

UserModel user = getExistingUser(session, realm, authSession);

if (user.isEmailVerified()) {
logger.debug("User " + user.getUsername() + " is already verified, skipping cleanup");
context.success();
return;
}

logger.debug("Cleaning up unverified user: " + user.getUsername());
SubjectCredentialManager manager = user.credentialManager();

manager.getStoredCredentialsByTypeStream(PasswordCredentialModel.TYPE)
.forEach(c -> {
logger.debug("Removing credential: " + c.getId() + " for user: " + user.getUsername());
manager.removeStoredCredentialById(c.getId());
});

Stream<FederatedIdentityModel> linkedAccounts = session.users().getFederatedIdentitiesStream(realm, user);

linkedAccounts.forEach(identity -> {
Comment on lines +54 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Stream<FederatedIdentityModel> linkedAccounts = session.users().getFederatedIdentitiesStream(realm, user);
linkedAccounts.forEach(identity -> {
session.users().getFederatedIdentitiesStream(realm, user).forEach(identity -> {

logger.debug("Removing federated identity: " + identity.getIdentityProvider() + " for user: " + user.getUsername());
session.users().removeFederatedIdentity(realm, user, identity.getIdentityProvider());
});

context.success();
}

@Override
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx,
BrokeredIdentityContext brokerContext) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package tech.neon.custom;

import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.List;

public class NeonCleanUnverifiedAuthenticatorFactory implements AuthenticatorFactory {

public static final String PROVIDER_ID = "neon-clean-unverified";
static NeonCleanUnverifiedAuthenticator SINGLETON = new NeonCleanUnverifiedAuthenticator();

@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}

@Override
public void init(Config.Scope config) {
}

@Override
public void postInit(KeycloakSessionFactory factory) {
}

@Override
public void close() {
}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public String getReferenceCategory() {
return null;
}

@Override
public boolean isConfigurable() {
return false;
}

@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return new AuthenticationExecutionModel.Requirement[] {
AuthenticationExecutionModel.Requirement.REQUIRED
};
}

@Override
public String getDisplayType() {
return "Clean Unverified User";
}

@Override
public String getHelpText() {
return "Cleans up unverified user data by removing passwords and social links";
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}

@Override
public boolean isUserSetupAllowed() {
return false;
}
}
82 changes: 82 additions & 0 deletions src/main/java/tech/neon/custom/NeonIdpCreateUserIfUnique.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package tech.neon.custom;

import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import java.util.List;
import java.util.Map;

import org.jboss.logging.Logger;

public class NeonIdpCreateUserIfUnique extends AbstractIdpAuthenticator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public class NeonIdpCreateUserIfUnique extends AbstractIdpAuthenticator {
public class NeonIdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator {


private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
private static Logger logger = Logger.getLogger(NeonIdpCreateUserIfUnique.class);


@Override
public boolean requiresUser() {
return false;
}

@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be true like in IdpCreateUserIfUniqueAuthenticator?

}

@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx,
BrokeredIdentityContext brokerContext) {
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();

String email = brokerContext.getEmail();

UserModel existingUser = context.getSession().users().getUserByEmail(context.getRealm(), email);

if (existingUser == null) {
logger.debugf(
"No duplication detected. Creating account for user '%s' and linking with identity provider '%s' .",
email, brokerContext.getIdpConfig().getAlias());

UserModel federatedUser = session.users().addUser(realm, email);
federatedUser.setEnabled(true);

if (Boolean.TRUE.equals(brokerContext.getContextData().get(NeonIdpEmailVerifyAuthenticator.VERIFIED_EMAIL))) {
federatedUser.setEmailVerified(true);
logger.debug("Email verified successfully for user: " + federatedUser.getEmail());

}

for (Map.Entry<String, List<String>> attr : serializedCtx.getAttributes().entrySet().stream()
.sorted(Map.Entry.comparingByKey()).toList()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to sort here?

if (!UserModel.USERNAME.equalsIgnoreCase(attr.getKey())) {
federatedUser.setAttribute(attr.getKey(), attr.getValue());
}
}

context.setUser(federatedUser);
context.getAuthenticationSession().setAuthNote(BROKER_REGISTERED_NEW_USER, "true");
context.success();
} else {
ExistingUserInfo duplication = new ExistingUserInfo(existingUser.getId(), UserModel.EMAIL, existingUser.getEmail());
logger.debugf("Duplication detected. There is already existing user with %s '%s' .",
duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue());

// Set duplicated user, so next authenticators can deal with it
context.getAuthenticationSession().setAuthNote(EXISTING_USER_INFO, duplication.serialize());
context.attempted();
}
}

@Override
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx,
BrokeredIdentityContext brokerContext) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package tech.neon.custom;

import java.util.List;

import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.Config;


public class NeonIdpCreateUserIfUniqueFactory implements AuthenticatorFactory {

public static final String PROVIDER_ID = "neon-idp-create-user-if-unique";
static NeonIdpCreateUserIfUnique SINGLETON = new NeonIdpCreateUserIfUnique();

@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}

@Override
public void init(Config.Scope config) {

}

@Override
public void postInit(KeycloakSessionFactory factory) {

}

@Override
public void close() {

}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public String getReferenceCategory() {
return null;
}

@Override
public boolean isConfigurable() {
return false;
}


@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}

@Override
public String getDisplayType() {
return "Create User If Unique";
}

@Override
public String getHelpText() {
return "Create User If Unique";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's clarify here how our implementation is different from the default one.

}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}

@Override
public boolean isUserSetupAllowed() {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package tech.neon.custom;

import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.sessions.AuthenticationSessionModel;

import org.jboss.logging.Logger;

public class NeonIdpEmailVerifyAuthenticator extends AbstractIdpAuthenticator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public class NeonIdpEmailVerifyAuthenticator extends AbstractIdpAuthenticator {
public class NeonIdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator {

public static final String VERIFIED_EMAIL = "VERIFIED_EMAIL";
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
private static Logger logger = Logger.getLogger(NeonIdpEmailVerifyAuthenticator.class);


@Override
public boolean requiresUser() {
return false;
}

@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
}

@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx,
BrokeredIdentityContext brokerContext) {
logger.debug("Starting email verification authentication for user: " + brokerContext.getEmail());

KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
AuthenticationSessionModel authSession = context.getAuthenticationSession();

if (brokerContext.getIdpConfig().isTrustEmail()
|| Boolean.TRUE.equals(brokerContext.getContextData().get(VERIFIED_EMAIL))) {
logger.debug("Email is trusted or already verified. Proceeding with authentication.");

UserModel user = getExistingUser(session, realm, authSession);
user.setEmailVerified(true);
logger.debug("Email verified successfully for user: " + user.getEmail());
context.success();

} else {
logger.debug("Email verification attempted but not trusted/verified for: " + brokerContext.getEmail());
context.attempted();
}
}

@Override
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx,
BrokeredIdentityContext brokerContext) {
logger.warn("Action implementation called for email verification");
}

}
Loading