Compare commits
4 Commits
9db284388b
...
main
Author | SHA1 | Date | |
---|---|---|---|
288029fe31 | |||
cd738a4a6f | |||
86cfd9cf57 | |||
cc4c0867f1 |
12
pom.xml
12
pom.xml
@ -1,6 +1,6 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>net.micedre.keycloak</groupId>
|
<groupId>com.github.thomasdarimont.keycloak</groupId>
|
||||||
<artifactId>keycloak-mail-whitelisting</artifactId>
|
<artifactId>keycloak-mail-whitelisting</artifactId>
|
||||||
<version>1.9-SNAPSHOT</version>
|
<version>1.9-SNAPSHOT</version>
|
||||||
|
|
||||||
@ -32,7 +32,7 @@
|
|||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
<keycloak.version>22.0.0</keycloak.version>
|
<keycloak.version>23.0.0</keycloak.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@ -60,6 +60,12 @@
|
|||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
<version>${keycloak.version}</version>
|
<version>${keycloak.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.auto.service</groupId>
|
||||||
|
<artifactId>auto-service</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
@ -149,4 +155,4 @@
|
|||||||
</build>
|
</build>
|
||||||
</profile>
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
</project>
|
</project>
|
||||||
|
@ -0,0 +1,230 @@
|
|||||||
|
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<String, String> 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<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 = 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<ProviderConfigProperty> 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<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return CONFIG_PROPERTIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void buildPage(FormContext context, LoginFormsProvider form) {
|
||||||
|
List<String> authorizedMailDomains = Arrays.asList(
|
||||||
|
context.getAuthenticatorConfig().getConfig().getOrDefault(domainListConfigName,DEFAULT_DOMAIN_LIST).split(DOMAIN_LIST_SEPARATOR));
|
||||||
|
form.setAttribute("authorizedMailDomains", authorizedMailDomains);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package net.micedre.keycloak.registration;
|
package com.github.thomasdarimont.keycloak.auth;
|
||||||
|
|
||||||
import org.keycloak.authentication.FormContext;
|
import org.keycloak.authentication.FormContext;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
@ -1,4 +1,4 @@
|
|||||||
package net.micedre.keycloak.registration;
|
package com.github.thomasdarimont.keycloak.auth;
|
||||||
|
|
||||||
import org.keycloak.authentication.FormContext;
|
import org.keycloak.authentication.FormContext;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
@ -69,4 +69,4 @@ public class RegistrationProfileWithMailDomainCheck extends RegistrationProfileD
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,110 +0,0 @@
|
|||||||
package net.micedre.keycloak.registration;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
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.KeycloakSession;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.utils.FormMessage;
|
|
||||||
import org.keycloak.services.messages.Messages;
|
|
||||||
import org.keycloak.services.validation.Validation;
|
|
||||||
import org.keycloak.storage.adapter.AbstractUserAdapter;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.core.MultivaluedMap;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public abstract class RegistrationProfileDomainValidation extends RegistrationProfile implements FormAction {
|
|
||||||
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 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 = getDomainList(mailDomainConfig);
|
|
||||||
|
|
||||||
boolean emailDomainValid = isEmailValid(email, domainList);
|
|
||||||
|
|
||||||
if (!emailDomainValid) {
|
|
||||||
// add user to a "waiting" group
|
|
||||||
// show a message saying you need to be approved by admin
|
|
||||||
KeycloakSession session = context.getSession();
|
|
||||||
RealmModel realm = context.getRealm();
|
|
||||||
AbstractUserAdapter user = new AbstractUserAdapter();
|
|
||||||
user.addRequiredAction("USER_MUST_BE_APPROVED");
|
|
||||||
setRequiredActions(context.getSession(), context.getRealm(), user);
|
|
||||||
|
|
||||||
context.success();
|
|
||||||
}
|
|
||||||
if (errors.size() > 0) {
|
|
||||||
context.error(eventError);
|
|
||||||
context.validationError(formData, errors);
|
|
||||||
} else {
|
|
||||||
context.success();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract String[] getDomainList(AuthenticatorConfigModel mailDomainConfig);
|
|
||||||
|
|
||||||
public abstract boolean isEmailValid(String email, String[] domains);
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
|||||||
net.micedre.keycloak.registration.RegistrationProfileWithMailDomainCheck
|
com.github.thomasdarimont.keycloak.auth.RegistrationProfileDomainValidation
|
||||||
net.micedre.keycloak.registration.RegistrationProfileWithDomainBlock
|
com.github.thomasdarimont.keycloak.auth.RegistrationProfileWithDomainBlock
|
||||||
|
com.github.thomasdarimont.keycloak.auth.RegistrationProfileWithMailDomainCheck
|
||||||
|
Reference in New Issue
Block a user