Skip to content

Commit ffa2e17

Browse files
authored
Fixed attribute validation error message (#4)
* simplified tests * simplified tests * Cucumber tests! * tests polish * bumped Keycloak version to 13.0.1 * renamed job * renamed job * test fix * fixed attribute validation message * version of build for automation tests is set from commit SHA
1 parent a2d3bbe commit ffa2e17

File tree

10 files changed

+211
-38
lines changed

10 files changed

+211
-38
lines changed

.github/workflows/automation-tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ jobs:
1818
java-version: '11'
1919
distribution: 'adopt'
2020

21+
- name: Set version from git commit SHA
22+
run: mvn -B -ntp versions:set -DgenerateBackupPoms=false -DnewVersion="${GITHUB_SHA::6}"
23+
2124
- name: Build authenticator jar file
2225
run: mvn -B -ntp package
2326

2427
- name: Build test docker container
25-
run: docker-compose build
28+
run: docker-compose build --build-arg VERSION="${GITHUB_SHA::6}"
2629

2730
- name: Run automation tests
2831
run: mvn -B -ntp test -P automation-tests -D selenide.headless=true

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Keycloak username password attribute authenticator
2-
[![CI with 🐋 & Selenide](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/actions/workflows/automation-tests.yml/badge.svg)](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/actions/workflows/automation-tests.yml)
1+
# Keycloak username password attribute
2+
[![automation tests](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/actions/workflows/automation-tests.yml/badge.svg)](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/actions/workflows/automation-tests.yml)
33
![Maven Central](https://img.shields.io/maven-central/v/io.github.kilmajster/keycloak-username-password-attribute-authenticator)
44
![Docker Image Version (latest by date)](https://img.shields.io/docker/v/kilmajster/keycloak-username-password-attribute-authenticator?label=docker%20hub)
55
![Docker Pulls](https://img.shields.io/docker/pulls/kilmajster/keycloak-username-password-attribute-authenticator)
@@ -9,11 +9,14 @@
99
Keycloak default login form with user attribute validation.
1010

1111
## How 2 use
12+
To use this authenticator, it should be bundled together with Keycloak, here are two ways how to do that:
1213
### using jar
1314

1415

1516

1617
### using docker init container
18+
If you want to use this authenticator in some cloud envirenement, here is ready init container. Jar file is placed in `/opt/jboss/keycloak/standalone/deployments`,
19+
so same location as target one. Possible
1720
```
1821
kilmajster/keycloak-username-password-attribute-authenticator:latest
1922
```
@@ -22,14 +25,14 @@ kilmajster/keycloak-username-password-attribute-authenticator:latest
2225
## Configuration
2326
### Authenticator config
2427
#### config via Keycloak UI / API
25-
- login_form_user_attribute
26-
- login_form_generate_label
27-
- login_form_attribute_label
28+
- login_form_user_attribute
29+
- login_form_generate_label
30+
- login_form_attribute_label
2831

2932
#### config via env variables
30-
- LOGIN_FORM_USER_ATTRIBUTE
31-
- LOGIN_FORM_GENERATE_LABEL
32-
- LOGIN_FORM_ATTRIBUTE_LABEL
33+
- LOGIN_FORM_USER_ATTRIBUTE
34+
- LOGIN_FORM_GENERATE_LABEL
35+
- LOGIN_FORM_ATTRIBUTE_LABEL
3336

3437
### Theme config
3538
#### Using bundled default keycloak theme

src/main/docker/dev.Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
FROM quay.io/keycloak/keycloak:latest
22

3-
ADD target/keycloak-username-password-attribute-authenticator-SNAPSHOT.jar /opt/jboss/keycloak/standalone/deployments
3+
ARG VERSION=SNAPSHOT
4+
5+
ADD target/keycloak-username-password-attribute-authenticator-${VERSION}.jar /opt/jboss/keycloak/standalone/deployments

src/main/java/io.github.kilmajster.keycloak/UsernamePasswordAttributeForm.java

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;
1010
import org.keycloak.events.Details;
1111
import org.keycloak.events.Errors;
12+
import org.keycloak.forms.login.LoginFormsProvider;
1213
import org.keycloak.models.ModelDuplicateException;
1314
import org.keycloak.models.UserModel;
15+
import org.keycloak.models.utils.FormMessage;
1416
import org.keycloak.models.utils.KeycloakModelUtils;
1517
import org.keycloak.services.ServicesLogger;
1618
import org.keycloak.services.managers.AuthenticationManager;
1719
import org.keycloak.services.messages.Messages;
1820
import org.keycloak.services.validation.Validation;
21+
import org.keycloak.theme.FreeMarkerUtil;
1922

2023
import javax.ws.rs.core.MultivaluedMap;
2124
import javax.ws.rs.core.Response;
@@ -26,6 +29,34 @@ public class UsernamePasswordAttributeForm extends UsernamePasswordForm implemen
2629

2730
protected static ServicesLogger log = ServicesLogger.LOGGER;
2831

32+
@Override
33+
protected Response challenge(AuthenticationFlowContext context, String error, String field) {
34+
LoginFormsProvider form = context.form().setExecution(context.getExecution().getId());
35+
if (error != null) {
36+
if (field != null) {
37+
form.addError(new FormMessage(field, error));
38+
} else {
39+
form.setError(error, new Object[0]);
40+
}
41+
}
42+
43+
configureUserAttributeLabel(context);
44+
45+
return createLoginForm(form);
46+
}
47+
48+
@Override
49+
protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
50+
LoginFormsProvider forms = context.form();
51+
if (formData.size() > 0) {
52+
forms.setFormData(formData);
53+
}
54+
55+
configureUserAttributeLabel(context);
56+
57+
return forms.createLoginUsernamePassword();
58+
}
59+
2960
@Override
3061
public void authenticate(AuthenticationFlowContext context) {
3162
MultivaluedMap<String, String> formData = new MultivaluedMapImpl();
@@ -40,9 +71,7 @@ public void authenticate(AuthenticationFlowContext context) {
4071
}
4172
}
4273

43-
configureUserAttributeLabel(context);
44-
45-
Response challengeResponse = this.challenge(context, formData);
74+
Response challengeResponse = challenge(context, formData);
4675
context.challenge(challengeResponse);
4776
}
4877

@@ -55,7 +84,7 @@ private void configureUserAttributeLabel(AuthenticationFlowContext context) {
5584
if (userAttributeName != null && !userAttributeName.isEmpty()) {
5685
context.form().setAttribute(USER_ATTRIBUTE_LABEL,
5786
isGeneratePropertyLabelEnabled(context)
58-
? UserAttributeLabelGenerator.from(userAttributeName)
87+
? UserAttributeLabelGenerator.generateLabel(userAttributeName)
5988
: userAttributeName);
6089
} else {
6190
log.warn("Configuration of keycloak-user-attribute-authenticator is incomplete! " +
@@ -96,7 +125,32 @@ private boolean isProvidedAttributeValid(AuthenticationFlowContext context, User
96125
private boolean invalidUserAttributeHandler(AuthenticationFlowContext context, UserModel user, boolean isAttributeEmpty) {
97126
context.getEvent().user(user);
98127
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
99-
Response challengeResponse = challenge(context, getDefaultChallengeMessage(context), USER_ATTRIBUTE);
128+
129+
String errorText;
130+
final String userAttributeErrorLabel = configPropertyOf(context, USER_ATTRIBUTE_ERROR_LABEL);
131+
if (userAttributeErrorLabel != null && !userAttributeErrorLabel.isBlank()) {
132+
// error text directly from error label propertyModelException
133+
errorText = userAttributeErrorLabel;
134+
} else {
135+
final String userAttributeLabel = configPropertyOf(context, USER_ATTRIBUTE_LABEL);
136+
if (userAttributeLabel != null && !userAttributeLabel.isBlank()) {
137+
// get message from message.properties in case USER_ATTRIBUTE_LABEL is a message key
138+
final String message = context.form().getMessage(userAttributeLabel);
139+
// generating error message based on provided user attribute label
140+
errorText = UserAttributeLabelGenerator.generateErrorText(message != null ? message : userAttributeLabel);
141+
} else {
142+
// user attribute label not provided so generating text based on attribute name
143+
errorText = isGeneratePropertyLabelEnabled(context) // generate pretty error if property is not disabled
144+
? UserAttributeLabelGenerator.generateErrorText(configPropertyOf(context, USER_ATTRIBUTE))
145+
: "Invalid ".concat(configPropertyOf(context, USER_ATTRIBUTE)); // use raw attribute name
146+
}
147+
}
148+
149+
if (isClearUserOnFailedAttributeValidationEnabled(context)) {
150+
context.clearUser();
151+
}
152+
153+
Response challengeResponse = challenge(context, errorText, USER_ATTRIBUTE);
100154

101155
if (isAttributeEmpty) {
102156
context.forceChallenge(challengeResponse);

src/main/java/io.github.kilmajster.keycloak/UsernamePasswordAttributeFormConfiguration.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public interface UsernamePasswordAttributeFormConfiguration {
1212
String USER_ATTRIBUTE = "login_form_user_attribute";
1313
String GENERATE_FORM_LABEL = "login_form_generate_label";
1414
String USER_ATTRIBUTE_LABEL = "login_form_attribute_label";
15+
String USER_ATTRIBUTE_ERROR_LABEL = "login_form_attribute_error_label";
16+
String CLEAR_USER_ON_ATTRIBUTE_VALIDATION_FAIL = "clear_user_on_attribute_validation_fail";
1517

1618
List<ProviderConfigProperty> PROPS = ProviderConfigurationBuilder.create()
1719

@@ -30,13 +32,28 @@ public interface UsernamePasswordAttributeFormConfiguration {
3032
.helpText("TODO")
3133
.add()
3234

35+
.property()
36+
.name(CLEAR_USER_ON_ATTRIBUTE_VALIDATION_FAIL)
37+
.type(ProviderConfigProperty.BOOLEAN_TYPE)
38+
.label("Clear user on attribute validation fail")
39+
.defaultValue(true)
40+
.helpText("TODO")
41+
.add()
42+
3343
.property()
3444
.name(USER_ATTRIBUTE_LABEL)
3545
.type(ProviderConfigProperty.STRING_TYPE)
3646
.label("User attribute form label")
3747
.helpText("TODO")
3848
.add()
3949

50+
.property()
51+
.name(USER_ATTRIBUTE_ERROR_LABEL)
52+
.type(ProviderConfigProperty.STRING_TYPE)
53+
.label("Message for user attribute validation error")
54+
.helpText("TODO")
55+
.add()
56+
4057
.build();
4158

4259
static String configPropertyOf(final AuthenticationFlowContext context, final String configPropertyName) {
@@ -47,4 +64,8 @@ static String configPropertyOf(final AuthenticationFlowContext context, final St
4764
static boolean isGeneratePropertyLabelEnabled(final AuthenticationFlowContext context) {
4865
return Boolean.parseBoolean(configPropertyOf(context, GENERATE_FORM_LABEL));
4966
}
67+
68+
static boolean isClearUserOnFailedAttributeValidationEnabled(final AuthenticationFlowContext context) {
69+
return Boolean.parseBoolean(configPropertyOf(context, CLEAR_USER_ON_ATTRIBUTE_VALIDATION_FAIL));
70+
}
5071
}

src/main/java/io.github.kilmajster.keycloak/utils/UserAttributeLabelGenerator.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
public final class UserAttributeLabelGenerator {
44

5-
public static String from(final String attributeName) {
5+
public static String generateLabel(final String attributeName) {
66
final String lowercaseWithSpaces = attributeName
77
.toLowerCase()
8+
.replace(".", " ")
89
.replace("_", " ")
910
.replace("-", " ");
1011

1112
return capitalizeFirstChar(lowercaseWithSpaces);
1213
}
1314

15+
public static String generateErrorText(final String attributeName) {
16+
return "Invalid " + generateLabel(attributeName).toLowerCase() + ".";
17+
}
18+
1419
private static String capitalizeFirstChar(final String lowercaseWithSpaces) {
1520
return lowercaseWithSpaces.substring(0,1).toUpperCase() + lowercaseWithSpaces.substring(1).toLowerCase();
1621
}

src/test/java/io.github.kilmajster.keycloak/KeycloakSteps.java

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import io.cucumber.java.en.Given;
77
import io.cucumber.java.en.Then;
88
import io.cucumber.java.en.When;
9-
import org.junit.Before;
109
import org.openqa.selenium.By;
1110
import org.slf4j.Logger;
1211
import org.slf4j.LoggerFactory;
1312
import org.testcontainers.containers.output.Slf4jLogConsumer;
1413

14+
import static com.codeborne.selenide.Condition.exactText;
1515
import static com.codeborne.selenide.Condition.text;
1616
import static com.codeborne.selenide.Selenide.$;
1717
import static com.codeborne.selenide.Selenide.open;
@@ -34,44 +34,58 @@ public void keycloak_is_running_with_default_setup() {
3434
}
3535
}
3636

37-
@Given("keycloak is running with LOGIN_FORM_ATTRIBUTE_LABEL = {string}")
38-
public void keycloak_is_running_with_login_form_attribute_label_env(final String envLoginFormAttributeLabel) {
39-
keycloak.addEnv("LOGIN_FORM_ATTRIBUTE_LABEL", envLoginFormAttributeLabel);
37+
@Given("keycloak is running with {} = {}")
38+
public void keycloak_is_running_with_env(final String envKey, final String envValue) {
39+
keycloak.addEnv(envKey, envValue);
4040
if (!keycloak.isRunning()) {
41-
log.info("Starting keycloak container with LOGIN_FORM_ATTRIBUTE_LABEL = " + envLoginFormAttributeLabel);
41+
log.info("Starting keycloak container with " + envKey + " = " + envValue);
4242
keycloak.start();
4343
}
4444
}
4545

4646
@When("user goes to the account console page")
4747
public void go_to_keycloak_account_page() {
4848
final String keycloakUrl = TestConstants.KEYCLOAK_LOCAL_URL_PREFIX + keycloak.getFirstMappedPort();
49+
50+
log.info("go_to_keycloak_account_page() :: keycloakUrl = " + keycloakUrl);
51+
4952
open(keycloakUrl + "/auth/realms/dev-realm/account");
5053
}
5154

5255
@Then("user should be not logged in")
5356
public void user_should_be_not_logged_in() {
57+
log.info("user_should_be_not_logged_in()");
58+
5459
final String loggedInUser = $(By.id("landingLoggedInUser")).val();
5560
assertThat(loggedInUser).isNullOrEmpty();
5661
}
5762

5863
@When("user clicks a sign in button")
59-
public static void click_sign_in_button() {
64+
public void click_sign_in_button() {
6065
log.info("click_sign_in_button()");
6166

6267
$(By.id("landingSignInButton")).click();
6368
}
6469

70+
@When("user navigates to login page")
71+
public void user_navigates_to_login_page() {
72+
log.info("user_navigates_to_login_page()");
73+
74+
go_to_keycloak_account_page();
75+
user_should_be_not_logged_in();
76+
click_sign_in_button();
77+
}
78+
6579
@Then("login form with attribute input labeled as {string} should be shown")
66-
public static void verify_login_form_is_displayed_with_user_attribute_label(final String label) {
80+
public void verify_login_form_is_displayed_with_user_attribute_label(final String label) {
6781
log.info("verify_login_form_is_displayed_with_user_attribute_label( label = " + label + " )");
6882

6983
assertThat($(By.id("kc-form-login")).isDisplayed()).isTrue();
7084
userAttributeFormLabel().shouldHave(text(label));
7185
}
7286

7387
@When("user log into account console with a valid credentials and user attribute equal {string}")
74-
public static void log_into_account_console(final String attribute) {
88+
public void log_into_account_console(final String attribute) {
7589
log.info("log_into_account_console()");
7690

7791
$(By.id("username")).val(TEST_USERNAME);
@@ -81,13 +95,41 @@ public static void log_into_account_console(final String attribute) {
8195
}
8296

8397
@Then("user should be logged into account console")
84-
public static void verify_that_user_is_logged_in() {
98+
public void verify_that_user_is_logged_in() {
8599
log.info("verify_that_user_is_logged_in()");
86100

87101
$(By.id("landingLoggedInUser")).shouldHave(text("test"));
88102
}
89103

90-
private static SelenideElement userAttributeFormLabel() {
104+
@Then("form error with message {string} is present")
105+
public void form_error_with_message_is_present(final String errorMessage) {
106+
log.info("form_error_with_message_is_present( " + errorMessage + " )");
107+
108+
$(By.className("alert-error")).shouldHave(text(errorMessage));
109+
}
110+
111+
@And("attempted username is cleared")
112+
public void attempted_username_is_cleared() {
113+
log.info("attempted_username_is_cleared()");
114+
115+
assertThat($(By.id("kc-attempted-username")).exists()).isFalse();
116+
}
117+
118+
@And("attempted username is set to {string}")
119+
public void attempted_username_is_set_to(final String attemptedUsername) {
120+
log.info("attempted_username_is_set_to( " + attemptedUsername + " )");
121+
122+
$(By.id("kc-attempted-username")).shouldHave(exactText(attemptedUsername));
123+
}
124+
125+
@And("restart login link is visible")
126+
public void restart_login_link_is_visible() {
127+
log.info("restart_login_link_is_visible()");
128+
129+
assertThat($(By.className("kc-tooltip-text")).getOwnText()).isEqualTo("Restart login");
130+
}
131+
132+
private SelenideElement userAttributeFormLabel() {
91133
return $(By.xpath("//input[@id='login_form_user_attribute']/preceding-sibling::label"));
92134
}
93-
}
135+
}

src/test/java/io.github.kilmajster.keycloak/utils/UserAttributeLabelGeneratorTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ public class UserAttributeLabelGeneratorTest {
1010

1111
@Test
1212
public void shouldPrettifyString() {
13-
final String attribute = "TEST_ATTRIBUTE-NAME";
13+
final String attribute = "TEST_ATTRIBUTE-NAME.bLah";
1414

15-
final String label = UserAttributeLabelGenerator.from(attribute);
15+
final String label = UserAttributeLabelGenerator.generateLabel(attribute);
1616

17-
assertThat(label).isEqualTo("Test attribute name");
17+
assertThat(label).isEqualTo("Test attribute name blah");
1818
}
1919
}

0 commit comments

Comments
 (0)