-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MS custom idp & more custom execution steps (#29)
- Loading branch information
Showing
12 changed files
with
625 additions
and
1 deletion.
There are no files selected for viewing
67 changes: 67 additions & 0 deletions
67
src/main/java/tech/neon/custom/NeonCleanUnverifiedAuthenticator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
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()); | ||
}); | ||
|
||
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) { | ||
|
||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
src/main/java/tech/neon/custom/NeonCleanUnverifiedAuthenticatorFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
86 changes: 86 additions & 0 deletions
86
src/main/java/tech/neon/custom/NeonIdpCreateUserIfUniqueAuthenticator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
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; | ||
|
||
/** | ||
* Custom implementation of Keycloak's IdP user creation authenticator. | ||
* This authenticator functions similarly to the built-in IdpCreateUserIfUniqueAuthenticator, | ||
* but adds support for the VERIFIED_EMAIL flag set by NeonIdpEmailVerificationAuthenticator. | ||
*/ | ||
public class NeonIdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator { | ||
|
||
private static Logger logger = Logger.getLogger(NeonIdpCreateUserIfUniqueAuthenticator.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(); | ||
|
||
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(NeonIdpEmailVerificationAuthenticator.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()) { | ||
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) { | ||
} | ||
|
||
} |
79 changes: 79 additions & 0 deletions
79
src/main/java/tech/neon/custom/NeonIdpCreateUserIfUniqueFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 NeonIdpCreateUserIfUniqueAuthenticator SINGLETON = new NeonIdpCreateUserIfUniqueAuthenticator(); | ||
|
||
@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"; | ||
} | ||
|
||
@Override | ||
public List<ProviderConfigProperty> getConfigProperties() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public boolean isUserSetupAllowed() { | ||
return false; | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
src/main/java/tech/neon/custom/NeonIdpEmailVerificationAuthenticator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package tech.neon.custom; | ||
|
||
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.KeycloakSession; | ||
import org.keycloak.models.RealmModel; | ||
import org.keycloak.models.UserModel; | ||
import org.keycloak.sessions.AuthenticationSessionModel; | ||
|
||
import org.jboss.logging.Logger; | ||
|
||
public class NeonIdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator { | ||
public static final String VERIFIED_EMAIL = "VERIFIED_EMAIL"; | ||
private static Logger logger = Logger.getLogger(NeonIdpEmailVerificationAuthenticator.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"); | ||
} | ||
|
||
} |
Oops, something went wrong.