Compare commits

...

2 Commits

Author SHA1 Message Date
288029fe31 wip: move logic from validate to success 2023-12-11 15:17:55 +01:00
cd738a4a6f wip: compiles and doesn't break startup 2023-12-08 16:07:46 +01:00
3 changed files with 194 additions and 73 deletions

View File

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

View File

@ -1,21 +1,27 @@
package com.github.thomasdarimont.keycloak.auth; package com.github.thomasdarimont.keycloak.auth;
// //
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException;
import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.ValidationContext; import org.keycloak.authentication.ValidationContext;
import org.keycloak.authentication.forms.RegistrationPage; import org.keycloak.authentication.forms.RegistrationPage;
import org.keycloak.authentication.forms.RegistrationProfile;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; 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.messages.Messages;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.userprofile.UserProfile;
import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.MultivaluedMap;
import java.util.Arrays;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
// //
@ -26,90 +32,199 @@ import org.keycloak.events.Errors;
import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
public abstract class RegistrationProfileDomainValidation extends RegistrationUserCreation { import com.google.auto.service.AutoService;
protected static final Logger logger = Logger.getLogger(RegistrationProfileDomainValidation.class);
protected static final String DEFAULT_DOMAIN_LIST = "example.org"; @AutoService(FormActionFactory.class)
protected static final String DOMAIN_LIST_SEPARATOR = "##"; public class RegistrationProfileDomainValidation extends RegistrationUserCreation {
protected static final Logger logger = Logger.getLogger(RegistrationProfileDomainValidation.class);
@Override protected static final String DEFAULT_DOMAIN_LIST = "example.org";
public boolean isConfigurable() { protected static final String DOMAIN_LIST_SEPARATOR = "##";
return true;
}
protected static final boolean globmatches(String text, String glob) { @Override
if (text.length() > 200) { public boolean isConfigurable() {
return false; return true;
} }
String rest = null;
int pos = glob.indexOf('*'); protected static final boolean globmatches(String text, String glob) {
if (pos != -1) { if (text.length() > 200) {
rest = glob.substring(pos + 1); return false;
glob = glob.substring(0, pos); }
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;
}
}
if (glob.length() > text.length()) @Override
return false; public void success(FormContext context) {
// handle the part up to the first * if (context.getUser() != null) {
for (int i = 0; i < glob.length(); i++) // the user probably did some back navigation in the browser, hitting this page in a strange state
if (glob.charAt(i) != '?' context.getEvent().detail(Details.EXISTING_USER, context.getUser().getUsername());
&& !glob.substring(i, i + 1).equalsIgnoreCase(text.substring(i, i + 1))) throw new AuthenticationFlowException(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, Errors.DIFFERENT_USER_AUTHENTICATING, Messages.EXPIRED_ACTION);
return false; }
// recurse for the part after the first *, if any MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
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 String email = formData.getFirst(UserModel.EMAIL);
public void validate(ValidationContext context) { String username = formData.getFirst(UserModel.USERNAME);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>(); if (context.getRealm().isRegistrationEmailAsUsername()) {
String email = formData.getFirst(Validation.FIELD_EMAIL); username = email;
}
// get the allowlist of mail domains
AuthenticatorConfigModel mailDomainConfig = context.getAuthenticatorConfig();
String eventError = Errors.INVALID_REGISTRATION;
AuthenticatorConfigModel mailDomainConfig = context.getAuthenticatorConfig(); String[] domainList = getDomainList(mailDomainConfig);
String eventError = Errors.INVALID_REGISTRATION;
if(email == null){ boolean emailDomainValid = isEmailValid(email, domainList);
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); context.getEvent().detail(Details.USERNAME, username).detail(Details.REGISTER_METHOD, "form").detail(Details.EMAIL, email);
boolean emailDomainValid = isEmailValid(email, domainList); UserProfile profile = getOrCreateUserProfile(context, formData);
UserModel user = profile.create();
if (!emailDomainValid)
user.addRequiredAction("USER_MUST_BE_APPROVED");
if (!emailDomainValid) { user.setEnabled(true);
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);
} context.setUser(user);
if (errors.size() > 0) {
context.error(eventError);
context.validationError(formData, errors);
} else {
context.success();
}
}
public abstract String[] getDomainList(AuthenticatorConfigModel mailDomainConfig); 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);
}
public abstract boolean isEmailValid(String email, String[] domains);
} }

View File

@ -1,3 +1,3 @@
com.thomasdarimont.keycloak.auth.CustomRegistrationUserCreation com.github.thomasdarimont.keycloak.auth.RegistrationProfileDomainValidation
com.thomasdarimont.keycloak.auth.RegistrationProfileWithDomainBlock com.github.thomasdarimont.keycloak.auth.RegistrationProfileWithDomainBlock
com.thomasdarimont.keycloak.auth.RegistrationProfileWithMailDomainCheck com.github.thomasdarimont.keycloak.auth.RegistrationProfileWithMailDomainCheck