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

Merged
merged 11 commits into from
Dec 23, 2024
Merged
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 -> {
prepor marked this conversation as resolved.
Show resolved Hide resolved
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 {
prepor marked this conversation as resolved.
Show resolved Hide resolved

private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
prepor marked this conversation as resolved.
Show resolved Hide resolved

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

@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
kszafran marked this conversation as resolved.
Show resolved Hide resolved
}

@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()) {
kszafran marked this conversation as resolved.
Show resolved Hide resolved
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";
kszafran marked this conversation as resolved.
Show resolved Hide resolved
}

@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 {
prepor marked this conversation as resolved.
Show resolved Hide resolved
public static final String VERIFIED_EMAIL = "VERIFIED_EMAIL";
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
prepor marked this conversation as resolved.
Show resolved Hide resolved

@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