package com.github.thomasdarimont.keycloak.auth; // import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowException; import org.keycloak.authentication.FormAction; import org.keycloak.authentication.ValidationContext; import org.keycloak.authentication.forms.RegistrationPage; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.messages.Messages; import org.keycloak.services.validation.Validation; import org.keycloak.userprofile.UserProfile; import jakarta.ws.rs.core.MultivaluedMap; import java.util.Arrays; import java.util.ArrayList; import java.util.List; // import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormContext; import org.keycloak.authentication.forms.RegistrationUserCreation; import org.keycloak.events.Errors; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.utils.FormMessage; import com.google.auto.service.AutoService; @AutoService(FormActionFactory.class) public class RegistrationProfileDomainValidation extends RegistrationUserCreation { protected static final Logger logger = Logger.getLogger(RegistrationProfileDomainValidation.class); protected static final String DEFAULT_DOMAIN_LIST = "example.org"; protected static final String DOMAIN_LIST_SEPARATOR = "##"; @Override public boolean isConfigurable() { return true; } protected static final boolean globmatches(String text, String glob) { if (text.length() > 200) { return false; } String rest = null; int pos = glob.indexOf('*'); if (pos != -1) { rest = glob.substring(pos + 1); glob = glob.substring(0, pos); } if (glob.length() > text.length()) return false; // handle the part up to the first * for (int i = 0; i < glob.length(); i++) if (glob.charAt(i) != '?' && !glob.substring(i, i + 1).equalsIgnoreCase(text.substring(i, i + 1))) return false; // recurse for the part after the first *, if any if (rest == null) { return glob.length() == text.length(); } else { for (int i = glob.length(); i <= text.length(); i++) { if (globmatches(text.substring(i), rest)) return true; } return false; } } @Override public void success(FormContext context) { if (context.getUser() != null) { // the user probably did some back navigation in the browser, hitting this page in a strange state context.getEvent().detail(Details.EXISTING_USER, context.getUser().getUsername()); throw new AuthenticationFlowException(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, Errors.DIFFERENT_USER_AUTHENTICATING, Messages.EXPIRED_ACTION); } MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); String email = formData.getFirst(UserModel.EMAIL); String username = formData.getFirst(UserModel.USERNAME); if (context.getRealm().isRegistrationEmailAsUsername()) { username = email; } // get the allowlist of mail domains AuthenticatorConfigModel mailDomainConfig = context.getAuthenticatorConfig(); String eventError = Errors.INVALID_REGISTRATION; String[] domainList = getDomainList(mailDomainConfig); boolean emailDomainValid = isEmailValid(email, domainList); context.getEvent().detail(Details.USERNAME, username).detail(Details.REGISTER_METHOD, "form").detail(Details.EMAIL, email); UserProfile profile = getOrCreateUserProfile(context, formData); UserModel user = profile.create(); if (!emailDomainValid) user.addRequiredAction("USER_MUST_BE_APPROVED"); user.setEnabled(true); context.setUser(user); if (!emailDomainValid) { user.addRequiredAction("USER_MUST_BE_APPROVED"); } context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); context.getEvent().user(user); context.getEvent().success(); context.newEvent().event(EventType.LOGIN); context.getEvent().client(context.getAuthenticationSession().getClient().getClientId()) .detail(Details.REDIRECT_URI, context.getAuthenticationSession().getRedirectUri()) .detail(Details.AUTH_METHOD, context.getAuthenticationSession().getProtocol()); String authType = context.getAuthenticationSession().getAuthNote(Details.AUTH_TYPE); if (authType != null) { context.getEvent().detail(Details.AUTH_TYPE, authType); } } /* @Override public void validate(ValidationContext context) { MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); List errors = new ArrayList<>(); String email = formData.getFirst(Validation.FIELD_EMAIL); AuthenticatorConfigModel mailDomainConfig = context.getAuthenticatorConfig(); String eventError = Errors.INVALID_REGISTRATION; if(email == null){ context.getEvent().detail(Details.EMAIL, email); errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL)); context.error(eventError); context.validationError(formData, errors); return; } String[] domainList = getDomainList(mailDomainConfig); boolean emailDomainValid = isEmailValid(email, domainList); if (!emailDomainValid) { super.success(context); KeycloakSession session = context.getSession(); RealmModel realm = context.getRealm(); UserModel user = context.getUser(); user.addRequiredAction("USER_MUST_BE_APPROVED"); setRequiredActions(session, realm, user); } if (errors.size() > 0) { context.error(eventError); context.validationError(formData, errors); } else { context.success(); } */ public String[] getDomainList(AuthenticatorConfigModel mailDomainConfig) { return mailDomainConfig.getConfig().getOrDefault(domainListConfigName, DEFAULT_DOMAIN_LIST).split(DOMAIN_LIST_SEPARATOR); } public boolean isEmailValid(String email, String[] domains) { for (String domain : domains) { if (email.endsWith("@" + domain) || email.equals(domain) || globmatches(email, "*@" + domain)) { return true; } } return false; } public static final String PROVIDER_ID = "registration-mail-check-action"; private static final List CONFIG_PROPERTIES = new ArrayList<>(); public static String domainListConfigName = "validDomains"; static { ProviderConfigProperty property; property = new ProviderConfigProperty(); property.setName(domainListConfigName); property.setLabel("Valid domains for emails"); property.setType(ProviderConfigProperty.STRING_TYPE); property.setHelpText("List mail domains authorized to register, separated by '##'"); CONFIG_PROPERTIES.add(property); } @Override public String getDisplayType() { return "Profile Validation with email domain check"; } @Override public String getId() { return PROVIDER_ID; } @Override public String getHelpText() { return "Adds validation of domain emails for registration"; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override public void buildPage(FormContext context, LoginFormsProvider form) { List authorizedMailDomains = Arrays.asList( context.getAuthenticatorConfig().getConfig().getOrDefault(domainListConfigName,DEFAULT_DOMAIN_LIST).split(DOMAIN_LIST_SEPARATOR)); form.setAttribute("authorizedMailDomains", authorizedMailDomains); } }