Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions packages/keycloak-extensions/voter-enrollment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<artifactId>message-otp-authenticator</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> HIDDEN_PROFILE_ATTRIBUTES = Set.of(UserModel.LOCALE);

@Override
public String getHelpText() {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -614,10 +618,16 @@ private MultivaluedMap<String, String> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2026 Sequent Tech <legal@sequentech.io>
//
// 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<String, String> 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<String, String> normalized =
(MultivaluedMap<String, String>)
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));
}
}
Loading