Skip to content

Commit 7b20c79

Browse files
authored
Added docs (#5)
* 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 * [skip ci] Renamed some configuration descs / AT polish / added docs part & imgs * [skip ci] Renamed some configuration descs / AT polish / added docs part & imgs * [skip ci] Renamed some configuration descs / AT polish / added docs part & imgs * updated readme & refactored automation tests accordingly to readme * readme polish * [skip ci] readme polish
1 parent ffa2e17 commit 7b20c79

12 files changed

+148
-68
lines changed
118 KB
Loading

.github/img/foot-size-form-config.png

25.1 KB
Loading

.github/img/foot-size-form-error.png

24.1 KB
Loading

.github/img/foot-size-form.png

22.6 KB
Loading
111 KB
Loading

.github/workflows/automation-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ jobs:
1919
distribution: 'adopt'
2020

2121
- name: Set version from git commit SHA
22-
run: mvn -B -ntp versions:set -DgenerateBackupPoms=false -DnewVersion="${GITHUB_SHA::6}"
22+
run: mvn -B -ntp versions:set -DgenerateBackupPoms=false -DnewVersion="${GITHUB_SHA::7}"
2323

2424
- name: Build authenticator jar file
2525
run: mvn -B -ntp package
2626

2727
- name: Build test docker container
28-
run: docker-compose build --build-arg VERSION="${GITHUB_SHA::6}"
28+
run: docker-compose build --build-arg VERSION="${GITHUB_SHA::7}"
2929

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

README.md

Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,124 @@
1-
# Keycloak username password attribute
1+
# Keycloak username password attribute authenticator
22
[![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)
66
![GitHub](https://img.shields.io/github/license/kilmajster/keycloak-username-password-attribute-authenticator)
77

88
## Description
9-
Keycloak default login form with user attribute validation.
9+
Keycloak default login form with additional user attribute validation. Example:
1010

11-
## How 2 use
11+
<p align="center">
12+
<img alt="Login form preview" src="/.github/img/foot-size-form.png" width="48%">
13+
&nbsp; &nbsp;
14+
<img alt="Form error message preview" src="/.github/img/foot-size-form-error.png" width="48%">
15+
</p>
16+
17+
## Usage
1218
To use this authenticator, it should be bundled together with Keycloak, here are two ways how to do that:
13-
### using jar
1419

20+
### Deploying jar file
21+
To deploy custom Keycloak extension it needs to be placed in `{$KEYCLOAK_PATH}/standalone/deployments/`.
22+
Latest authenticator jar file can be downloaded from
23+
[Github Releases](https://github.com/kilmajster/keycloak-username-password-attribute-authenticator/releases/latest) page or
24+
[Maven Central Repository](https://mvnrepository.com/artifact/io.github.kilmajster/keycloak-username-password-attribute-authenticator/latest).
1525

26+
### Using Docker init container
27+
If you want to use this authenticator in cloud environment, here is ready [init container](https://hub.docker.com/r/kilmajster/keycloak-username-password-attribute-authenticator).
28+
Jar file is placed in `/opt/jboss/keycloak/standalone/deployments`, so same location as target one.
29+
According to official Keycloak [example](https://github.com/codecentric/helm-charts/blob/master/charts/keycloak/README.md#providing-a-custom-theme),
30+
Helm chart could look like following:
31+
```yaml
32+
extraInitContainers: |
33+
- name: attribute-authenticator-provider
34+
image: kilmajster/keycloak-username-password-attribute-authenticator:latest
35+
imagePullPolicy: IfNotPresent
36+
command:
37+
- sh
38+
args:
39+
- -c
40+
- |
41+
echo "Copying attribute authenticator..."
42+
cp -R /opt/jboss/keycloak/standalone/deployments/*.jar /attribute-authenticator
43+
volumeMounts:
44+
- name: attribute-authenticator
45+
mountPath: /attribute-authenticator
1646
17-
### 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
20-
```
21-
kilmajster/keycloak-username-password-attribute-authenticator:latest
22-
```
23-
#### example helm chart snippet
47+
extraVolumeMounts: |
48+
- name: attribute-authenticator
49+
mountPath: /opt/jboss/keycloak/standalone/deployments
50+
51+
extraVolumes: |
52+
- name: attribute-authenticator
53+
emptyDir: {}
54+
```
2455
2556
## Configuration
26-
### Authenticator config
27-
#### config via Keycloak UI / API
57+
### Authentication configuration
58+
<p align="center">
59+
<img src="/.github/img/new-authenticator-execution.png" alt="New authentication execution">
60+
</p>
61+
62+
<p align="center">
63+
<img src="/.github/img/foot-size-execution-config-tooltip.png" alt="Form config tooltip">
64+
</p>
65+
66+
#### Minimal configuration
2867
- login_form_user_attribute
29-
- login_form_generate_label
30-
- login_form_attribute_label
3168
32-
#### config via env variables
33-
- LOGIN_FORM_USER_ATTRIBUTE
69+
<p align="center">
70+
<img src="/.github/img/foot-size-form-config.png" alt="Authenticator configuration">
71+
</p>
72+
73+
#### Advanced configuration
74+
- login_form_generate_label
75+
- login_form_attribute_label
76+
- login_form_error_message
77+
- clear_user_on_attribute_validation_fail
78+
##### config via Keycloak API
79+
TODO
80+
##### Configuration via environment variables
3481
- LOGIN_FORM_GENERATE_LABEL
3582
- LOGIN_FORM_ATTRIBUTE_LABEL
83+
- LOGIN_FORM_ERROR_MESSAGE
84+
- CLEAR_USER_ON_ATTRIBUTE_VALIDATION_FAIL
3685
37-
### Theme config
86+
### Theme configuration
3887
#### Using bundled default keycloak theme
88+
- choose theme `base-with-attribute`
89+
- override authentication flow to `Browser with user attribute`
90+
3991
#### Extending own theme
92+
```html
93+
...
94+
<div class="${properties.kcFormGroupClass!}">
95+
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
96+
97+
<input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="off"
98+
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
99+
/>
100+
</div>
101+
102+
<!-- keycloak-user-attribute-authenticator custom code block start -->
103+
<div class="${properties.kcFormGroupClass!}">
104+
<label for="login_form_user_attribute" class="${properties.kcLabelClass!}">
105+
<#if login_form_attribute_label??>
106+
${msg(login_form_attribute_label)}
107+
<#else>
108+
${msg("login_form_attribute_label_default")}
109+
</#if>
110+
</label>
111+
112+
<input tabindex="3" id="login_form_user_attribute" class="${properties.kcInputClass!}"
113+
name="login_form_user_attribute" type="text" autocomplete="off"
114+
aria-invalid="<#if messagesPerField.existsError('login_form_user_attribute')>true</#if>"
115+
/>
116+
</div>
117+
<!-- keycloak-user-attribute-authenticator custom code block end -->
118+
119+
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
120+
...
121+
```
40122

41123
-------------------------------------
42124
### Development
@@ -63,6 +145,5 @@ $ mvn test -P automation-tests
63145
```
64146
##### running tests in docker
65147
```shell
66-
$ mvn test -P automation-tests -D headless=true
67-
```
68-
148+
$ mvn test -P automation-tests -D selenide.headless=true
149+
```

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

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import org.keycloak.services.managers.AuthenticationManager;
1919
import org.keycloak.services.messages.Messages;
2020
import org.keycloak.services.validation.Validation;
21-
import org.keycloak.theme.FreeMarkerUtil;
2221

2322
import javax.ws.rs.core.MultivaluedMap;
2423
import javax.ws.rs.core.Response;
@@ -76,14 +75,14 @@ public void authenticate(AuthenticationFlowContext context) {
7675
}
7776

7877
private void configureUserAttributeLabel(AuthenticationFlowContext context) {
79-
final String userAttributeLabel = configPropertyOf(context, USER_ATTRIBUTE_LABEL);
78+
final String userAttributeLabel = configPropertyOf(context, LOGIN_FORM_ATTRIBUTE_LABEL);
8079
if (userAttributeLabel != null) {
81-
context.form().setAttribute(USER_ATTRIBUTE_LABEL, userAttributeLabel);
80+
context.form().setAttribute(LOGIN_FORM_ATTRIBUTE_LABEL, userAttributeLabel);
8281
} else {
83-
final String userAttributeName = configPropertyOf(context, USER_ATTRIBUTE);
82+
final String userAttributeName = configPropertyOf(context, LOGIN_FORM_USER_ATTRIBUTE);
8483
if (userAttributeName != null && !userAttributeName.isEmpty()) {
85-
context.form().setAttribute(USER_ATTRIBUTE_LABEL,
86-
isGeneratePropertyLabelEnabled(context)
84+
context.form().setAttribute(LOGIN_FORM_ATTRIBUTE_LABEL,
85+
isGenerateLabelEnabled(context)
8786
? UserAttributeLabelGenerator.generateLabel(userAttributeName)
8887
: userAttributeName);
8988
} else {
@@ -103,7 +102,7 @@ && validatePassword(context, user, formData) && validateUser(context, user, form
103102
}
104103

105104
private boolean validateUserAttribute(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> formData) {
106-
final String providedAttribute = formData.getFirst(USER_ATTRIBUTE);
105+
final String providedAttribute = formData.getFirst(LOGIN_FORM_USER_ATTRIBUTE);
107106
if (providedAttribute == null || providedAttribute.isEmpty()) {
108107
return invalidUserAttributeHandler(context, user, true);
109108
}
@@ -116,7 +115,7 @@ private boolean validateUserAttribute(AuthenticationFlowContext context, UserMod
116115
}
117116

118117
private boolean isProvidedAttributeValid(AuthenticationFlowContext context, UserModel user, String providedUserAttribute) {
119-
String userAttributeName = context.getAuthenticatorConfig().getConfig().get(USER_ATTRIBUTE);
118+
String userAttributeName = context.getAuthenticatorConfig().getConfig().get(LOGIN_FORM_USER_ATTRIBUTE);
120119
return user.getAttributeStream(userAttributeName)
121120
.anyMatch(attr -> attr.equals(providedUserAttribute));
122121
}
@@ -127,30 +126,30 @@ private boolean invalidUserAttributeHandler(AuthenticationFlowContext context, U
127126
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
128127

129128
String errorText;
130-
final String userAttributeErrorLabel = configPropertyOf(context, USER_ATTRIBUTE_ERROR_LABEL);
131-
if (userAttributeErrorLabel != null && !userAttributeErrorLabel.isBlank()) {
129+
final String configuredErrorMessage = configPropertyOf(context, LOGIN_FORM_ERROR_MESSAGE);
130+
if (configuredErrorMessage != null && !configuredErrorMessage.isBlank()) {
132131
// error text directly from error label propertyModelException
133-
errorText = userAttributeErrorLabel;
132+
errorText = configuredErrorMessage;
134133
} else {
135-
final String userAttributeLabel = configPropertyOf(context, USER_ATTRIBUTE_LABEL);
134+
final String userAttributeLabel = configPropertyOf(context, LOGIN_FORM_ATTRIBUTE_LABEL);
136135
if (userAttributeLabel != null && !userAttributeLabel.isBlank()) {
137136
// get message from message.properties in case USER_ATTRIBUTE_LABEL is a message key
138137
final String message = context.form().getMessage(userAttributeLabel);
139138
// generating error message based on provided user attribute label
140139
errorText = UserAttributeLabelGenerator.generateErrorText(message != null ? message : userAttributeLabel);
141140
} else {
142141
// 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
142+
errorText = isGenerateLabelEnabled(context) // generate pretty error if property is not disabled
143+
? UserAttributeLabelGenerator.generateErrorText(configPropertyOf(context, LOGIN_FORM_USER_ATTRIBUTE))
144+
: "Invalid ".concat(configPropertyOf(context, LOGIN_FORM_USER_ATTRIBUTE)); // use raw attribute name
146145
}
147146
}
148147

149148
if (isClearUserOnFailedAttributeValidationEnabled(context)) {
150149
context.clearUser();
151150
}
152151

153-
Response challengeResponse = challenge(context, errorText, USER_ATTRIBUTE);
152+
Response challengeResponse = challenge(context, errorText, LOGIN_FORM_USER_ATTRIBUTE);
154153

155154
if (isAttributeEmpty) {
156155
context.forceChallenge(challengeResponse);

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,48 +9,48 @@
99

1010
public interface UsernamePasswordAttributeFormConfiguration {
1111

12-
String USER_ATTRIBUTE = "login_form_user_attribute";
13-
String GENERATE_FORM_LABEL = "login_form_generate_label";
14-
String USER_ATTRIBUTE_LABEL = "login_form_attribute_label";
15-
String USER_ATTRIBUTE_ERROR_LABEL = "login_form_attribute_error_label";
12+
String LOGIN_FORM_USER_ATTRIBUTE = "login_form_user_attribute";
13+
String LOGIN_FORM_GENERATE_LABEL = "login_form_generate_label";
14+
String LOGIN_FORM_ATTRIBUTE_LABEL = "login_form_attribute_label";
15+
String LOGIN_FORM_ERROR_MESSAGE = "login_form_error_message";
1616
String CLEAR_USER_ON_ATTRIBUTE_VALIDATION_FAIL = "clear_user_on_attribute_validation_fail";
1717

1818
List<ProviderConfigProperty> PROPS = ProviderConfigurationBuilder.create()
1919

2020
.property()
21-
.name(USER_ATTRIBUTE)
21+
.name(LOGIN_FORM_USER_ATTRIBUTE)
2222
.type(ProviderConfigProperty.STRING_TYPE)
23-
.label("User attribute key name")
23+
.label("User attribute")
2424
.helpText("TODO")
2525
.add()
2626

2727
.property()
28-
.name(GENERATE_FORM_LABEL)
28+
.name(LOGIN_FORM_GENERATE_LABEL)
2929
.type(ProviderConfigProperty.BOOLEAN_TYPE)
3030
.label("Generate label")
31-
.defaultValue(true)
31+
.defaultValue("true") // only string value is accepted
3232
.helpText("TODO")
3333
.add()
3434

3535
.property()
3636
.name(CLEAR_USER_ON_ATTRIBUTE_VALIDATION_FAIL)
3737
.type(ProviderConfigProperty.BOOLEAN_TYPE)
38-
.label("Clear user on attribute validation fail")
39-
.defaultValue(true)
38+
.label("Clear user on validation fail")
39+
.defaultValue("true") // only string value is accepted
4040
.helpText("TODO")
4141
.add()
4242

4343
.property()
44-
.name(USER_ATTRIBUTE_LABEL)
44+
.name(LOGIN_FORM_ATTRIBUTE_LABEL)
4545
.type(ProviderConfigProperty.STRING_TYPE)
4646
.label("User attribute form label")
4747
.helpText("TODO")
4848
.add()
4949

5050
.property()
51-
.name(USER_ATTRIBUTE_ERROR_LABEL)
51+
.name(LOGIN_FORM_ERROR_MESSAGE)
5252
.type(ProviderConfigProperty.STRING_TYPE)
53-
.label("Message for user attribute validation error")
53+
.label("Validation error message")
5454
.helpText("TODO")
5555
.add()
5656

@@ -61,8 +61,8 @@ static String configPropertyOf(final AuthenticationFlowContext context, final St
6161
.orElse(context.getAuthenticatorConfig().getConfig().get(configPropertyName));
6262
}
6363

64-
static boolean isGeneratePropertyLabelEnabled(final AuthenticationFlowContext context) {
65-
return Boolean.parseBoolean(configPropertyOf(context, GENERATE_FORM_LABEL));
64+
static boolean isGenerateLabelEnabled(final AuthenticationFlowContext context) {
65+
return Boolean.parseBoolean(configPropertyOf(context, LOGIN_FORM_GENERATE_LABEL));
6666
}
6767

6868
static boolean isClearUserOnFailedAttributeValidationEnabled(final AuthenticationFlowContext context) {

src/test/resources/cucumber/login-form-env-var-config.feature

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ Feature: Login form with user attribute and environment variable based configura
33
Scenario: label and error message are generated from attribute name when environment variable configuration is empty
44
Given keycloak is running with default setup
55
When user navigates to login page
6-
Then login form with attribute input labeled as "Test attr" should be shown
6+
Then login form with attribute input labeled as "Foot size" should be shown
77
When user log into account console with a valid credentials and user attribute equal "invalid-user-attribute"
8-
Then form error with message "Invalid test attr." is present
8+
Then form error with message "Invalid foot size." is present
99
And attempted username is cleared
1010

1111
Scenario: attempted username is not cleared when clearing is disabled via environment variable
1212
Given keycloak is running with CLEAR_USER_ON_ATTRIBUTE_VALIDATION_FAIL = false
1313
When user navigates to login page
14-
Then login form with attribute input labeled as "Test attr" should be shown
14+
Then login form with attribute input labeled as "Foot size" should be shown
1515
When user log into account console with a valid credentials and user attribute equal "invalid-user-attribute"
16-
Then form error with message "Invalid test attr." is present
16+
Then form error with message "Invalid foot size." is present
1717
And attempted username is set to "test"
1818
And restart login link is visible
1919

@@ -28,9 +28,9 @@ Feature: Login form with user attribute and environment variable based configura
2828
Scenario: label and error message are taken from attribute name without prettify
2929
Given keycloak is running with LOGIN_FORM_GENERATE_LABEL = false
3030
When user navigates to login page
31-
Then login form with attribute input labeled as "test_attr" should be shown
31+
Then login form with attribute input labeled as "foot_size" should be shown
3232
When user log into account console with a valid credentials and user attribute equal "invalid-user-attribute"
33-
Then form error with message "Invalid test_attr" is present
33+
Then form error with message "Invalid foot_size" is present
3434
And attempted username is cleared
3535

3636
Scenario: label and error message are taken from environment variable

0 commit comments

Comments
 (0)