-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 -> { | ||
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; | ||
} | ||
} |
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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
@Override | ||||||
public boolean requiresUser() { | ||||||
return false; | ||||||
} | ||||||
|
||||||
@Override | ||||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { | ||||||
return false; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't it be |
||||||
} | ||||||
|
||||||
@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()) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
public static final String VERIFIED_EMAIL = "VERIFIED_EMAIL"; | ||||||
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
@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"); | ||||||
} | ||||||
|
||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.