Skip to content

Commit e4d4570

Browse files
authored
Prevent the username field from being rendered when running the identity-first login flow
Closes #43091 Signed-off-by: Pedro Igor <[email protected]>
1 parent 6527b13 commit e4d4570

File tree

8 files changed

+56
-29
lines changed

8 files changed

+56
-29
lines changed

services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
package org.keycloak.authentication.authenticators.browser;
1919

20-
import org.jboss.logging.Logger;
2120
import org.keycloak.authentication.AbstractFormAuthenticator;
2221
import org.keycloak.authentication.AuthenticationFlowContext;
2322
import org.keycloak.authentication.AuthenticationFlowError;
@@ -37,6 +36,7 @@
3736

3837
import jakarta.ws.rs.core.MultivaluedMap;
3938
import jakarta.ws.rs.core.Response;
39+
import org.keycloak.sessions.AuthenticationSessionModel;
4040

4141
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
4242
import static org.keycloak.services.validation.Validation.FIELD_PASSWORD;
@@ -48,10 +48,16 @@
4848
*/
4949
public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuthenticator {
5050

51-
private static final Logger logger = Logger.getLogger(AbstractUsernameFormAuthenticator.class);
52-
53-
public static final String REGISTRATION_FORM_ACTION = "registration_form";
5451
public static final String ATTEMPTED_USERNAME = "ATTEMPTED_USERNAME";
52+
53+
/**
54+
* An authentication session not to indicate that the username field should be hidden.
55+
* This note is usually set together with {@link #ATTEMPTED_USERNAME} to indicated that the
56+
* user can restart the flow by choosing a different username.
57+
* It should be set by authenticators that happen before this authenticator in the flow so that the original intent
58+
* is kept when this authenticator is executed on subsequent requests.
59+
*/
60+
public static final String USERNAME_HIDDEN = "USERNAME_HIDDEN";
5561
public static final String SESSION_INVALID = "SESSION_INVALID";
5662

5763
// Flag is true if user was already set in the authContext before this authenticator was triggered. In this case we skip clearing of the user after unsuccessful password authentication
@@ -69,6 +75,14 @@ protected Response challenge(AuthenticationFlowContext context, String error) {
6975
protected Response challenge(AuthenticationFlowContext context, String error, String field) {
7076
LoginFormsProvider form = context.form()
7177
.setExecution(context.getExecution().getId());
78+
79+
AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
80+
81+
if (Boolean.parseBoolean(authenticationSession.getAuthNote(USERNAME_HIDDEN))) {
82+
// if username is hidden, shown errors in the password field instead
83+
field = FIELD_PASSWORD;
84+
}
85+
7286
if (error != null) {
7387
if (field != null) {
7488
form.addError(new FormMessage(field, error));

services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,9 @@ protected void createCommonAttributes(Theme theme, Locale locale, Properties mes
505505
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
506506
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
507507
attributes.put("auth", new AuthenticationContextBean(context, page));
508+
if (authenticationSession != null && Boolean.parseBoolean(authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.USERNAME_HIDDEN))) {
509+
attributes.put(LoginFormsProvider.USERNAME_HIDDEN, Boolean.TRUE.toString());
510+
}
508511
setAttribute(Constants.EXECUTION, execution);
509512

510513
if (realm.isInternationalizationEnabled()) {

services/src/main/java/org/keycloak/forms/login/freemarker/model/AuthenticationContextBean.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.keycloak.authentication.AuthenticationSelectionOption;
2626
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
2727
import org.keycloak.forms.login.LoginFormsPages;
28+
import org.keycloak.sessions.AuthenticationSessionModel;
2829

2930
/**
3031
* @author <a href="mailto:[email protected]">Marek Posolda</a>
@@ -49,7 +50,17 @@ public boolean showTryAnotherWayLink() {
4950

5051

5152
public boolean showUsername() {
52-
return context != null && context.getUser() != null && context.getAuthenticationSession() != null && page!=LoginFormsPages.ERROR;
53+
if (context == null) {
54+
return false;
55+
}
56+
57+
AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
58+
59+
if (Boolean.parseBoolean(authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.USERNAME_HIDDEN))) {
60+
return getAttemptedUsername() != null;
61+
}
62+
63+
return context.getUser() != null && authenticationSession != null && page!=LoginFormsPages.ERROR;
5364
}
5465

5566
public boolean showResetCredentials() {

services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -422,18 +422,7 @@ private void attempted(AuthenticationFlowContext context, String username) {
422422

423423
if (username != null) {
424424
authenticationSession.setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
425-
LoginFormsProvider form = context.form();
426-
427-
form.setAttributeMapper(attributes -> {
428-
AuthenticationContextBean auth = (AuthenticationContextBean) attributes.get("auth");
429-
430-
if (auth != null) {
431-
attributes.put("auth", new OrganizationAwareAuthenticationContextBean(auth, true, username));
432-
attributes.put(LoginFormsProvider.USERNAME_HIDDEN, true);
433-
}
434-
435-
return attributes;
436-
});
425+
authenticationSession.setAuthNote(AbstractUsernameFormAuthenticator.USERNAME_HIDDEN, Boolean.TRUE.toString());
437426
}
438427

439428
context.attempted();

services/src/main/java/org/keycloak/organization/forms/login/freemarker/model/OrganizationAwareAuthenticationContextBean.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,11 @@ public class OrganizationAwareAuthenticationContextBean extends AuthenticationCo
2626

2727
private final AuthenticationContextBean delegate;
2828
private final boolean showTryAnotherWayLink;
29-
private final String username;
3029

3130
public OrganizationAwareAuthenticationContextBean(AuthenticationContextBean delegate, boolean showTryAnotherWayLink) {
32-
this(delegate, showTryAnotherWayLink, null);
33-
}
34-
35-
public OrganizationAwareAuthenticationContextBean(AuthenticationContextBean delegate, boolean showTryAnotherWayLink, String username) {
3631
super(null, null);
3732
this.delegate = delegate;
3833
this.showTryAnotherWayLink = showTryAnotherWayLink;
39-
this.username = username;
4034
}
4135

4236
@Override
@@ -52,17 +46,14 @@ public boolean showTryAnotherWayLink() {
5246
}
5347

5448
public boolean showUsername() {
55-
return username != null || delegate.showUsername();
49+
return delegate.showUsername();
5650
}
5751

5852
public boolean showResetCredentials() {
5953
return delegate.showResetCredentials();
6054
}
6155

6256
public String getAttemptedUsername() {
63-
if (username == null) {
64-
return delegate.getAttemptedUsername();
65-
}
66-
return username;
57+
return delegate.getAttemptedUsername();
6758
}
6859
}

testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ public static void assertAttemptedUsernameAvailability(WebDriver driver, boolean
120120
try {
121121
driver.findElement(By.id("kc-attempted-username"));
122122
Assert.assertTrue(expectedAvailability);
123+
// make sure the username field is not shown if the attempted username field is present
124+
Assert.assertTrue(driver.findElements(By.id("username")).isEmpty());
123125
} catch (NoSuchElementException nse) {
124126
Assert.assertFalse(expectedAvailability);
125127
}

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/authentication/OrganizationAuthenticationTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,23 @@ public void testRestartLogin() {
295295
Assert.assertFalse(loginPage.isPasswordInputPresent());
296296
}
297297

298+
@Test
299+
public void testAttemptedUsernameKeptAfterPasswordFailures() {
300+
testRealm().organizations().get(createOrganization().getId());
301+
302+
openIdentityFirstLoginPage("[email protected]", false, null, false, false);
303+
304+
// check if the login page is shown
305+
loginPage.assertAttemptedUsernameAvailability(true);
306+
Assert.assertTrue(loginPage.isPasswordInputPresent());
307+
308+
for (int i = 0; i < 3; i++) {
309+
loginPage.login("wrong-password");
310+
loginPage.assertAttemptedUsernameAvailability(true);
311+
Assert.assertTrue(loginPage.isPasswordInputPresent());
312+
}
313+
}
314+
298315
private void runOnServer(RunOnServer function) {
299316
testingClient.server(bc.consumerRealmName()).run(function);
300317
}

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/broker/AbstractBrokerSelfRegistrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ public void testRealmLevelBrokersAvailableIfEmailDoesNotMatchOrganization() {
227227

228228
driver.navigate().refresh();
229229

230-
Assert.assertTrue(loginPage.isUsernameInputPresent());
230+
loginPage.assertAttemptedUsernameAvailability(true);
231231
Assert.assertTrue(loginPage.isPasswordInputPresent());
232232
Assert.assertTrue(loginPage.isSocialButtonPresent(idp.getAlias()));
233233
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));

0 commit comments

Comments
 (0)