From 81514b06a3e9fa3e41358e77dd2af8d989ca9f93 Mon Sep 17 00:00:00 2001 From: Henrique Okomura Date: Tue, 18 Apr 2023 22:29:40 -0300 Subject: [PATCH] feat: add RegistrationProfileWithDomainBlock --- README.md | 12 +- .../RegistrationProfileDomainValidation.java | 96 ++++++++++++ .../RegistrationProfileWithDomainBlock.java | 131 +++------------- ...egistrationProfileWithMailDomainCheck.java | 148 ++++-------------- 4 files changed, 159 insertions(+), 228 deletions(-) create mode 100644 src/main/java/net/micedre/keycloak/registration/RegistrationProfileDomainValidation.java diff --git a/README.md b/README.md index 0e93db4..4eee4d5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Keycloak - Whitelist email domain for registration +# Keycloak - Email domain validation for registration -This extension allows you to validate email domain used for registration in keycloak to accept only a finite list of domain. +This extension allows you to validate email domain used for registration in keycloak to accept or deny a finite list of domain. You can use basic [glob syntax](https://en.wikipedia.org/wiki/Glob_(programming)) (only `*` and `?` are supported) @@ -23,15 +23,15 @@ The plugin directory is `$KEYCLOAK_HOME\providers`. - Go to the admin console, in authentication menu. - Copy the registration flow -- add a new execution below "Profile Validation" and choose "Profile Validation With Email Domain Check" +- add a new execution below "Profile Validation" and choose "Profile Validation With Email Domain Check" or "Profile Validation with domain block" - Set the execution "Required" -- Configure this new execution (otherwise, keycloak will only accept "exemple.org" domains) +- Configure this new execution with the allowed or blocked domains, otherwise, keycloak will only accept or block "exemple.org" domains - Change the registration binding to this new flow - Configure the realm to accept registration and verify email (this is important!) -## Display authorized mail domains in register forms +## Display mail domains in register forms -This extension provides the list of authorized patterns in the `authorizedMailDomains` attribute of the registration page. +This extension provides the list of authorized patterns in the `authorizedMailDomains` and `unauthorizedMailDomains` attribute of the registration page. This can be used like this : diff --git a/src/main/java/net/micedre/keycloak/registration/RegistrationProfileDomainValidation.java b/src/main/java/net/micedre/keycloak/registration/RegistrationProfileDomainValidation.java new file mode 100644 index 0000000..a853649 --- /dev/null +++ b/src/main/java/net/micedre/keycloak/registration/RegistrationProfileDomainValidation.java @@ -0,0 +1,96 @@ +package net.micedre.keycloak.registration; + +import org.keycloak.authentication.FormAction; +import org.keycloak.authentication.ValidationContext; +import org.keycloak.authentication.forms.RegistrationPage; +import org.keycloak.authentication.forms.RegistrationProfile; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.utils.FormMessage; +import org.keycloak.services.messages.Messages; +import org.keycloak.services.validation.Validation; + +import javax.ws.rs.core.MultivaluedMap; +import java.util.ArrayList; +import java.util.List; + +public abstract class RegistrationProfileDomainValidation extends RegistrationProfile implements FormAction { + + protected static String domainListConfigName; + 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 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 = mailDomainConfig.getConfig().getOrDefault(domainListConfigName, DEFAULT_DOMAIN_LIST).split(DOMAIN_LIST_SEPARATOR); + boolean emailDomainValid = isEmailValid(email, domainList); + + if (!emailDomainValid) { + context.getEvent().detail(Details.EMAIL, email); + errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL)); + } + if (errors.size() > 0) { + context.error(eventError); + context.validationError(formData, errors); + } else { + context.success(); + } + } + + public abstract boolean isEmailValid(String email, String[] domains); +} + diff --git a/src/main/java/net/micedre/keycloak/registration/RegistrationProfileWithDomainBlock.java b/src/main/java/net/micedre/keycloak/registration/RegistrationProfileWithDomainBlock.java index 72c0804..7d9986f 100644 --- a/src/main/java/net/micedre/keycloak/registration/RegistrationProfileWithDomainBlock.java +++ b/src/main/java/net/micedre/keycloak/registration/RegistrationProfileWithDomainBlock.java @@ -4,25 +4,26 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import javax.ws.rs.core.MultivaluedMap; - -import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormContext; -import org.keycloak.authentication.ValidationContext; -import org.keycloak.authentication.forms.RegistrationPage; -import org.keycloak.authentication.forms.RegistrationProfile; -import org.keycloak.events.Details; -import org.keycloak.events.Errors; import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.utils.FormMessage; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.services.messages.Messages; -import org.keycloak.services.validation.Validation; -public class RegistrationProfileWithDomainBlock extends RegistrationProfile implements FormAction { +public class RegistrationProfileWithDomainBlock extends RegistrationProfileDomainValidation { public static final String PROVIDER_ID = "registration-domain-block-action"; + private static final List CONFIG_PROPERTIES = new ArrayList<>(); + + static { + domainListConfigName = "invalidDomains"; + + ProviderConfigProperty property; + property = new ProviderConfigProperty(); + property.setName(domainListConfigName); + property.setLabel("Invalid domain for emails"); + property.setType(ProviderConfigProperty.STRING_TYPE); + property.setHelpText("List mail domains not authorized to register, separated by '##'"); + CONFIG_PROPERTIES.add(property); + } @Override public String getDisplayType() { @@ -34,111 +35,31 @@ public class RegistrationProfileWithDomainBlock extends RegistrationProfile impl return PROVIDER_ID; } - @Override - public boolean isConfigurable() { - return true; - } - @Override public String getHelpText() { return "Adds validation of not accepted domain emails for registration"; } - private static final List CONFIG_PROPERTIES = new ArrayList(); - - static { - ProviderConfigProperty property; - property = new ProviderConfigProperty(); - property.setName("invalidDomains"); - property.setLabel("Invalid domain for emails"); - property.setType(ProviderConfigProperty.STRING_TYPE); - property.setHelpText("List mail domains not authorized to register, separated by '##'"); - CONFIG_PROPERTIES.add(property); - } - - private 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 List getConfigProperties() { return CONFIG_PROPERTIES; } @Override - public void validate(ValidationContext context) { - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - - List errors = new ArrayList<>(); - String email = formData.getFirst(Validation.FIELD_EMAIL); - - boolean emailDomainValid = true; - 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[] domains = mailDomainConfig.getConfig().getOrDefault("invalidDomains", "example.com").split("##"); - for (String domain : domains) { - if (email.endsWith("@" + domain) || email.equals(domain)) { - emailDomainValid = false; - break; - } else if (globmatches(email, "*@" + domain)) { - emailDomainValid = false; - break; - } - } - if (!emailDomainValid) { - context.getEvent().detail(Details.EMAIL, email); - errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL)); - } - if (errors.size() > 0) { - context.error(eventError); - context.validationError(formData, errors); - } else { - context.success(); - } - + public void buildPage(FormContext context, LoginFormsProvider form) { + List unauthorizedMailDomains = Arrays.asList( + context.getAuthenticatorConfig().getConfig().getOrDefault(domainListConfigName, DEFAULT_DOMAIN_LIST).split(DOMAIN_LIST_SEPARATOR)); + form.setAttribute("unauthorizedMailDomains", unauthorizedMailDomains); } @Override - public void buildPage(FormContext context, LoginFormsProvider form) { - List unauthorizedMailDomains = Arrays.asList( - context.getAuthenticatorConfig().getConfig().getOrDefault("invalidDomains","exemple.org").split("##")); - form.setAttribute("unauthorizedMailDomains", unauthorizedMailDomains); + public boolean isEmailValid(String email, String[] domains) { + for (String domain : domains) { + if (email.endsWith("@" + domain) || email.equals(domain) || globmatches(email, "*@" + domain)) { + return false; + } + } + + return true; } } diff --git a/src/main/java/net/micedre/keycloak/registration/RegistrationProfileWithMailDomainCheck.java b/src/main/java/net/micedre/keycloak/registration/RegistrationProfileWithMailDomainCheck.java index ad421ac..6bf3596 100644 --- a/src/main/java/net/micedre/keycloak/registration/RegistrationProfileWithMailDomainCheck.java +++ b/src/main/java/net/micedre/keycloak/registration/RegistrationProfileWithMailDomainCheck.java @@ -1,152 +1,66 @@ package net.micedre.keycloak.registration; +import org.keycloak.authentication.FormContext; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.provider.ProviderConfigProperty; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import javax.ws.rs.core.MultivaluedMap; - -import org.keycloak.authentication.FormAction; -import org.keycloak.authentication.FormContext; -import org.keycloak.authentication.ValidationContext; -import org.keycloak.authentication.forms.RegistrationPage; -import org.keycloak.authentication.forms.RegistrationProfile; -import org.keycloak.events.Details; -import org.keycloak.events.Errors; -import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.utils.FormMessage; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.services.messages.Messages; -import org.keycloak.services.validation.Validation; - -public class RegistrationProfileWithMailDomainCheck extends RegistrationProfile implements FormAction { +public class RegistrationProfileWithMailDomainCheck extends RegistrationProfileDomainValidation { public static final String PROVIDER_ID = "registration-mail-check-action"; - @Override - public String getDisplayType() { - return "Profile Validation with email domain check"; + private static final List CONFIG_PROPERTIES = new ArrayList<>(); + + static { + domainListConfigName = "validDomains"; + + 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 boolean isConfigurable() { - return true; - } - - @Override public String getHelpText() { return "Adds validation of domain emails for registration"; } - private static final List CONFIG_PROPERTIES = new ArrayList(); - - static { - ProviderConfigProperty property; - property = new ProviderConfigProperty(); - property.setName("validDomains"); - property.setLabel("Valid domain for emails"); - property.setType(ProviderConfigProperty.STRING_TYPE); - property.setHelpText("List mail domains authorized to register, separated by '##'"); - CONFIG_PROPERTIES.add(property); - } - - - private 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 List getConfigProperties() { return CONFIG_PROPERTIES; } - @Override - public void validate(ValidationContext context) { - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - - List errors = new ArrayList<>(); - String email = formData.getFirst(Validation.FIELD_EMAIL); - - boolean emailDomainValid = false; - 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[] domains = mailDomainConfig.getConfig().getOrDefault("validDomains", "example.com").split("##"); - for (String domain : domains) { - if (email.endsWith("@" + domain) || email.equals(domain)) { - emailDomainValid = true; - break; - } else if (globmatches(email, "*@" + domain)) { - emailDomainValid = true; - break; - } - } - if (!emailDomainValid) { - context.getEvent().detail(Details.EMAIL, email); - errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL)); - } - if (errors.size() > 0) { - context.error(eventError); - context.validationError(formData, errors); - return; - - } else { - context.success(); - } - - } - - @Override public void buildPage(FormContext context, LoginFormsProvider form) { List authorizedMailDomains = Arrays.asList( - context.getAuthenticatorConfig().getConfig().getOrDefault("validDomains","exemple.org").split("##")); + context.getAuthenticatorConfig().getConfig().getOrDefault(domainListConfigName,DEFAULT_DOMAIN_LIST).split(DOMAIN_LIST_SEPARATOR)); form.setAttribute("authorizedMailDomains", authorizedMailDomains); } + @Override + 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; + } } \ No newline at end of file