mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-06 06:49:53 -06:00
Prevent the username field from being rendered when running the identity-first login flow
Closes #43091 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
@@ -17,7 +17,6 @@
|
||||
|
||||
package org.keycloak.authentication.authenticators.browser;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AbstractFormAuthenticator;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
@@ -37,6 +36,7 @@ import org.keycloak.services.messages.Messages;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
|
||||
import static org.keycloak.services.validation.Validation.FIELD_PASSWORD;
|
||||
@@ -48,10 +48,16 @@ import static org.keycloak.services.validation.Validation.FIELD_USERNAME;
|
||||
*/
|
||||
public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuthenticator {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AbstractUsernameFormAuthenticator.class);
|
||||
|
||||
public static final String REGISTRATION_FORM_ACTION = "registration_form";
|
||||
public static final String ATTEMPTED_USERNAME = "ATTEMPTED_USERNAME";
|
||||
|
||||
/**
|
||||
* An authentication session not to indicate that the username field should be hidden.
|
||||
* This note is usually set together with {@link #ATTEMPTED_USERNAME} to indicated that the
|
||||
* user can restart the flow by choosing a different username.
|
||||
* It should be set by authenticators that happen before this authenticator in the flow so that the original intent
|
||||
* is kept when this authenticator is executed on subsequent requests.
|
||||
*/
|
||||
public static final String USERNAME_HIDDEN = "USERNAME_HIDDEN";
|
||||
public static final String SESSION_INVALID = "SESSION_INVALID";
|
||||
|
||||
// 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 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
||||
protected Response challenge(AuthenticationFlowContext context, String error, String field) {
|
||||
LoginFormsProvider form = context.form()
|
||||
.setExecution(context.getExecution().getId());
|
||||
|
||||
AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
|
||||
|
||||
if (Boolean.parseBoolean(authenticationSession.getAuthNote(USERNAME_HIDDEN))) {
|
||||
// if username is hidden, shown errors in the password field instead
|
||||
field = FIELD_PASSWORD;
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
if (field != null) {
|
||||
form.addError(new FormMessage(field, error));
|
||||
|
||||
@@ -505,6 +505,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
||||
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
|
||||
attributes.put("auth", new AuthenticationContextBean(context, page));
|
||||
if (authenticationSession != null && Boolean.parseBoolean(authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.USERNAME_HIDDEN))) {
|
||||
attributes.put(LoginFormsProvider.USERNAME_HIDDEN, Boolean.TRUE.toString());
|
||||
}
|
||||
setAttribute(Constants.EXECUTION, execution);
|
||||
|
||||
if (realm.isInternationalizationEnabled()) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationSelectionOption;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.forms.login.LoginFormsPages;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@@ -49,7 +50,17 @@ public class AuthenticationContextBean {
|
||||
|
||||
|
||||
public boolean showUsername() {
|
||||
return context != null && context.getUser() != null && context.getAuthenticationSession() != null && page!=LoginFormsPages.ERROR;
|
||||
if (context == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
|
||||
|
||||
if (Boolean.parseBoolean(authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.USERNAME_HIDDEN))) {
|
||||
return getAttemptedUsername() != null;
|
||||
}
|
||||
|
||||
return context.getUser() != null && authenticationSession != null && page!=LoginFormsPages.ERROR;
|
||||
}
|
||||
|
||||
public boolean showResetCredentials() {
|
||||
|
||||
@@ -422,18 +422,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||
|
||||
if (username != null) {
|
||||
authenticationSession.setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
|
||||
LoginFormsProvider form = context.form();
|
||||
|
||||
form.setAttributeMapper(attributes -> {
|
||||
AuthenticationContextBean auth = (AuthenticationContextBean) attributes.get("auth");
|
||||
|
||||
if (auth != null) {
|
||||
attributes.put("auth", new OrganizationAwareAuthenticationContextBean(auth, true, username));
|
||||
attributes.put(LoginFormsProvider.USERNAME_HIDDEN, true);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
});
|
||||
authenticationSession.setAuthNote(AbstractUsernameFormAuthenticator.USERNAME_HIDDEN, Boolean.TRUE.toString());
|
||||
}
|
||||
|
||||
context.attempted();
|
||||
|
||||
@@ -26,17 +26,11 @@ public class OrganizationAwareAuthenticationContextBean extends AuthenticationCo
|
||||
|
||||
private final AuthenticationContextBean delegate;
|
||||
private final boolean showTryAnotherWayLink;
|
||||
private final String username;
|
||||
|
||||
public OrganizationAwareAuthenticationContextBean(AuthenticationContextBean delegate, boolean showTryAnotherWayLink) {
|
||||
this(delegate, showTryAnotherWayLink, null);
|
||||
}
|
||||
|
||||
public OrganizationAwareAuthenticationContextBean(AuthenticationContextBean delegate, boolean showTryAnotherWayLink, String username) {
|
||||
super(null, null);
|
||||
this.delegate = delegate;
|
||||
this.showTryAnotherWayLink = showTryAnotherWayLink;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,7 +46,7 @@ public class OrganizationAwareAuthenticationContextBean extends AuthenticationCo
|
||||
}
|
||||
|
||||
public boolean showUsername() {
|
||||
return username != null || delegate.showUsername();
|
||||
return delegate.showUsername();
|
||||
}
|
||||
|
||||
public boolean showResetCredentials() {
|
||||
@@ -60,9 +54,6 @@ public class OrganizationAwareAuthenticationContextBean extends AuthenticationCo
|
||||
}
|
||||
|
||||
public String getAttemptedUsername() {
|
||||
if (username == null) {
|
||||
return delegate.getAttemptedUsername();
|
||||
}
|
||||
return username;
|
||||
return delegate.getAttemptedUsername();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@ public abstract class LanguageComboboxAwarePage extends AbstractPage {
|
||||
try {
|
||||
driver.findElement(By.id("kc-attempted-username"));
|
||||
Assert.assertTrue(expectedAvailability);
|
||||
// make sure the username field is not shown if the attempted username field is present
|
||||
Assert.assertTrue(driver.findElements(By.id("username")).isEmpty());
|
||||
} catch (NoSuchElementException nse) {
|
||||
Assert.assertFalse(expectedAvailability);
|
||||
}
|
||||
|
||||
@@ -295,6 +295,23 @@ public class OrganizationAuthenticationTest extends AbstractOrganizationTest {
|
||||
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttemptedUsernameKeptAfterPasswordFailures() {
|
||||
testRealm().organizations().get(createOrganization().getId());
|
||||
|
||||
openIdentityFirstLoginPage("user@noorg.org", false, null, false, false);
|
||||
|
||||
// check if the login page is shown
|
||||
loginPage.assertAttemptedUsernameAvailability(true);
|
||||
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
loginPage.login("wrong-password");
|
||||
loginPage.assertAttemptedUsernameAvailability(true);
|
||||
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||
}
|
||||
}
|
||||
|
||||
private void runOnServer(RunOnServer function) {
|
||||
testingClient.server(bc.consumerRealmName()).run(function);
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
|
||||
|
||||
driver.navigate().refresh();
|
||||
|
||||
Assert.assertTrue(loginPage.isUsernameInputPresent());
|
||||
loginPage.assertAttemptedUsernameAvailability(true);
|
||||
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||
Assert.assertTrue(loginPage.isSocialButtonPresent(idp.getAlias()));
|
||||
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||
|
||||
Reference in New Issue
Block a user