diff --git a/packages/keycloak-extensions/sequent-theme/src/main/resources/theme/sequent.admin-portal/login/user-profile-commons.ftl b/packages/keycloak-extensions/sequent-theme/src/main/resources/theme/sequent.admin-portal/login/user-profile-commons.ftl index 027f7012d4..1e32dec0c4 100644 --- a/packages/keycloak-extensions/sequent-theme/src/main/resources/theme/sequent.admin-portal/login/user-profile-commons.ftl +++ b/packages/keycloak-extensions/sequent-theme/src/main/resources/theme/sequent.admin-portal/login/user-profile-commons.ftl @@ -12,6 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-only <#assign disabledElements = []> <#list profile.attributes as attribute> + <#if hiddenProfileAttributes?? && hiddenProfileAttributes?seq_contains(attribute.name)> + <#continue> + <#-- Check for default custom attribute and assign it the first time the form is opened --> <#if attribute.values?has_content> <#assign values = attribute.values> diff --git a/packages/keycloak-extensions/voter-enrollment/pom.xml b/packages/keycloak-extensions/voter-enrollment/pom.xml index ac203732bd..ae6fe6d1fb 100644 --- a/packages/keycloak-extensions/voter-enrollment/pom.xml +++ b/packages/keycloak-extensions/voter-enrollment/pom.xml @@ -25,6 +25,12 @@ SPDX-License-Identifier: AGPL-3.0-only message-otp-authenticator ${project.version} + + org.junit.jupiter + junit-jupiter + 6.0.3 + test + diff --git a/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreation.java b/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreation.java index 759c04ba51..8fe95371ba 100644 --- a/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreation.java +++ b/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreation.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.extern.jbosslog.JBossLog; @@ -94,6 +95,7 @@ public String getValue() { public static final String INVALID_INPUT = "Invalid input"; public static final String MISSING_FIELDS_ERROR = "error_user_attribute_required"; + public static final Set HIDDEN_PROFILE_ATTRIBUTES = Set.of(UserModel.LOCALE); @Override public String getHelpText() { @@ -220,6 +222,7 @@ public void validate(ValidationContext context) { && !Messages.USERNAME_EXISTS.equals(error.getMessage())) && !Messages.EMAIL_EXISTS.equals(error.getMessage()) // If username is hidden ignore the missing username validation error. + && !isLocaleRequiredError(error) && !(Messages.MISSING_USERNAME.equals(error.getMessage()) && "true" .equals( @@ -501,6 +504,7 @@ public void buildPage(FormContext context, LoginFormsProvider form) { form.setAttribute("passwordRequired", passwordRequired); form.setAttribute("formMode", formMode); + form.setAttribute("hiddenProfileAttributes", HIDDEN_PROFILE_ATTRIBUTES); log.infov("buildPage(): formMode = {0}", formMode); checkNotOtherUserAuthenticating(context); } @@ -614,10 +618,16 @@ private MultivaluedMap normalizeFormParameters( // user-profile data copy.remove(RegistrationPage.FIELD_PASSWORD); copy.remove(RegistrationPage.FIELD_PASSWORD_CONFIRM); + copy.remove(UserModel.LOCALE); return copy; } + static boolean isLocaleRequiredError(ValidationException.Error error) { + return UserModel.LOCALE.equals(error.getAttribute()) + && MISSING_FIELDS_ERROR.equals(error.getMessage()); + } + /** * Get user profile instance for current HTTP request (KeycloakSession) and for given context. * This assumes that there is single user registered within HTTP request, which is always the case diff --git a/packages/keycloak-extensions/voter-enrollment/src/test/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreationTest.java b/packages/keycloak-extensions/voter-enrollment/src/test/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreationTest.java new file mode 100644 index 0000000000..970db97314 --- /dev/null +++ b/packages/keycloak-extensions/voter-enrollment/src/test/java/sequent/keycloak/voter_enrollment/DeferredRegistrationUserCreationTest.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 Sequent Tech +// +// SPDX-License-Identifier: AGPL-3.0-only +package sequent.keycloak.voter_enrollment; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; +import org.keycloak.authentication.forms.RegistrationPage; +import org.keycloak.models.UserModel; +import org.keycloak.userprofile.ValidationException; +import org.keycloak.validate.ValidationError; + +class DeferredRegistrationUserCreationTest { + + @Test + void normalizeFormParametersRemovesLocaleAndSensitiveFields() throws Exception { + MultivaluedMap formParams = new MultivaluedHashMap<>(); + formParams.add(RegistrationPage.FIELD_PASSWORD, "password"); + formParams.add(RegistrationPage.FIELD_PASSWORD_CONFIRM, "password"); + formParams.add(UserModel.LOCALE, "en"); + formParams.add(UserModel.EMAIL, "voter@example.com"); + + Method method = + DeferredRegistrationUserCreation.class.getDeclaredMethod( + "normalizeFormParameters", MultivaluedMap.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + MultivaluedMap normalized = + (MultivaluedMap) + method.invoke(new DeferredRegistrationUserCreation(), formParams); + + assertFalse(normalized.containsKey(RegistrationPage.FIELD_PASSWORD)); + assertFalse(normalized.containsKey(RegistrationPage.FIELD_PASSWORD_CONFIRM)); + assertFalse(normalized.containsKey(UserModel.LOCALE)); + assertTrue(normalized.containsKey(UserModel.EMAIL)); + } + + @Test + void hiddenProfileAttributesIncludesLocale() { + assertTrue( + DeferredRegistrationUserCreation.HIDDEN_PROFILE_ATTRIBUTES.contains(UserModel.LOCALE)); + } + + @Test + void isLocaleRequiredErrorOnlyMatchesRequiredLocale() { + ValidationException.Error localeRequiredError = + new ValidationException.Error( + new ValidationError( + "required", + UserModel.LOCALE, + DeferredRegistrationUserCreation.MISSING_FIELDS_ERROR)); + ValidationException.Error localeInvalidError = + new ValidationException.Error( + new ValidationError( + "validator", UserModel.LOCALE, ValidationError.MESSAGE_INVALID_VALUE)); + ValidationException.Error emailRequiredError = + new ValidationException.Error( + new ValidationError( + "required", + UserModel.EMAIL, + DeferredRegistrationUserCreation.MISSING_FIELDS_ERROR)); + + assertTrue(DeferredRegistrationUserCreation.isLocaleRequiredError(localeRequiredError)); + assertFalse(DeferredRegistrationUserCreation.isLocaleRequiredError(localeInvalidError)); + assertFalse(DeferredRegistrationUserCreation.isLocaleRequiredError(emailRequiredError)); + } +}