feat: add RegistrationProfileWithDomainBlock

This commit is contained in:
Henrique Okomura 2023-04-18 22:29:40 -03:00 committed by Cédric Couralet
parent a0efd5999e
commit 81514b06a3
4 changed files with 159 additions and 228 deletions

View File

@ -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 :

View File

@ -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<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> 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);
}

View File

@ -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<ProviderConfigProperty> 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<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
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<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> 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<String> 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<String> 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;
}
}

View File

@ -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<ProviderConfigProperty> 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<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
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<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> 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<String> 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;
}
}