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>
+ #if>
<#-- 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));
+ }
+}