Skip to content

Commit

Permalink
MS custom idp & more custom execution steps (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
prepor authored Dec 23, 2024
1 parent 4bba1b6 commit 6265976
Show file tree
Hide file tree
Showing 12 changed files with 625 additions and 1 deletion.
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) {

}
}
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,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) {
}

}
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;
}
}
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");
}

}
Loading

0 comments on commit 6265976

Please sign in to comment.