diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java index cfb6722a474..ddbabbacfdb 100644 --- a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java @@ -23,7 +23,7 @@ import static org.keycloak.models.utils.KeycloakModelUtils.findUserByNameOrEmail import static org.keycloak.organization.utils.Organizations.getEmailDomain; import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent; import static org.keycloak.organization.utils.Organizations.resolveHomeBroker; -import static org.keycloak.utils.StringUtil.isNotBlank; +import static org.keycloak.utils.StringUtil.isBlank; import java.util.List; import java.util.Map; @@ -66,12 +66,9 @@ import org.keycloak.organization.utils.Organizations; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; -import org.keycloak.utils.StringUtil; public class OrganizationAuthenticator extends IdentityProviderAuthenticator { - private static final String LOGIN_HINT_ALREADY_HANDLED = "loginHintAlreadyHandled"; - private final KeycloakSession session; private final WebAuthnConditionalUIAuthenticator webauthnAuth; @@ -89,36 +86,28 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { return; } - String loginHint = session.getContext().getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM); - - if (isNotBlank(loginHint) && !"true".equals(context.getAuthenticationSession().getClientNote(LOGIN_HINT_ALREADY_HANDLED))) { - UserModel user = resolveUser(context, loginHint); - context.setUser(user); - - // set auth note to true to handle login_hint only once, we don't want to handle it again after a flow restart - context.getAuthenticationSession().setClientNote(LOGIN_HINT_ALREADY_HANDLED, "true"); - } - + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + String loginHint = authSession.getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM); OrganizationModel organization = Organizations.resolveOrganization(session); - if (organization == null) { + if (loginHint == null && organization == null) { initialChallenge(context); - } else { - // make sure the organization is set to the auth session to remember it when processing subsequent requests - AuthenticationSessionModel authSession = context.getAuthenticationSession(); - authSession.setAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); - action(context, false); + return; } + + if (organization != null) { + // make sure the organization is set to the auth session to remember it when processing subsequent requests + authSession.setAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); + } + + action(context, loginHint); } @Override public void action(AuthenticationFlowContext context) { - action(context, true); - } - - private void action(AuthenticationFlowContext context, boolean formSubmitted) { HttpRequest request = context.getHttpRequest(); MultivaluedMap parameters = request.getDecodedFormParameters(); + String username = parameters.getFirst(UserModel.USERNAME); // check if it's a webauthn submission and perform the webauth login if (webauthnAuth.isPasskeysEnabled() && (parameters.containsKey(WebAuthnConstants.AUTHENTICATOR_DATA) @@ -130,10 +119,9 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { } } - String username = parameters.getFirst(UserModel.USERNAME); UserModel user = context.getUser(); - if (formSubmitted && user == null && StringUtil.isBlank(username)) { + if (user == null && isBlank(username)) { initialChallenge(context, form -> { form.addError(new FormMessage(UserModel.USERNAME, Messages.INVALID_USERNAME)); return form.createLoginUsername(); @@ -141,8 +129,12 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { return; } + action(context, username); + } + + private void action(AuthenticationFlowContext context, String username) { + UserModel user = resolveUser(context, username); RealmModel realm = context.getRealm(); - user = resolveUser(context, username); String domain = getEmailDomain(username); OrganizationModel organization = resolveOrganization(user, domain); @@ -162,8 +154,8 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { } // remember the organization during the lifetime of the authentication session - AuthenticationSessionModel authenticationSession = context.getAuthenticationSession(); - authenticationSession.setAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + authSession.setAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); // make sure the organization is set to the session to make it available to templates session.getContext().setOrganization(organization); @@ -186,7 +178,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { return; } - if (isSSOAuthentication(authenticationSession)) { + if (isSSOAuthentication(authSession)) { // if re-authenticating in the scope of an organization context.success(); } else { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java index 0d66703b85c..08a9f9cb00b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java @@ -22,6 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; @@ -235,7 +236,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { if (firstTimeLogin) { waitForPage(driver, "update account information", false); updateAccountInformationPage.assertCurrent(); - Assert.assertTrue("We must be on correct realm right now", + assertTrue("We must be on correct realm right now", driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); log.debug("Updating info on updateAccount page"); assertFalse(driver.getPageSource().contains("kc.org")); @@ -267,7 +268,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { protected UserRepresentation getUserRepresentation(String realm, String userEmail) { UsersResource users = adminClient.realm(realm).users(); List reps = users.searchByEmail(userEmail, true); - Assert.assertFalse(reps.isEmpty()); + assertFalse(reps.isEmpty()); Assert.assertEquals(1, reps.size()); return reps.get(0); } @@ -300,14 +301,16 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { oauth.clientId("broker-app"); loginPage.open(bc.consumerRealmName()); log.debug("Logging in"); - Assert.assertFalse(loginPage.isPasswordInputPresent()); - Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias())); - Assert.assertTrue(loginPage.isRegisterLinkPresent()); + assertTrue(loginPage.isUsernameInputPresent()); + assertNull(loginPage.getUsernameInputError()); + assertFalse(loginPage.isPasswordInputPresent()); + assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias())); + assertTrue(loginPage.isRegisterLinkPresent()); if (idpAlias != null) { if (isVisible) { - Assert.assertTrue(loginPage.isSocialButtonPresent(idpAlias)); + assertTrue(loginPage.isSocialButtonPresent(idpAlias)); } else { - Assert.assertFalse(loginPage.isSocialButtonPresent(idpAlias)); + assertFalse(loginPage.isSocialButtonPresent(idpAlias)); } } loginPage.loginUsername(username); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/broker/AbstractBrokerSelfRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/broker/AbstractBrokerSelfRegistrationTest.java index 67e24cbd148..f63b9f46a05 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/broker/AbstractBrokerSelfRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/broker/AbstractBrokerSelfRegistrationTest.java @@ -77,6 +77,30 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz Assert.assertEquals(bc.getUserEmail(), loginPage.getUsername()); } + @Test + public void testLoginHintSentToBrokerIfUserAlreadyAMember() { + OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); + IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation(); + idp.getConfig().put(IdentityProviderModel.LOGIN_HINT, "true"); + testRealm().identityProviders().get(bc.getIDPAlias()).update(idp); + String userId = ApiUtil.getCreatedId(testRealm().users().create(UserBuilder.create() + .username("test") + .email("test@neworg.org") + .enabled(true) + .firstName("f") + .lastName("l") + .build())); + organization.members().addMember(userId).close(); + + // login hint will automatically redirect user to broker + oauth.realm(bc.consumerRealmName()); + oauth.client("broker-app"); + oauth.loginForm().loginHint("test@neworg.org").open(); + + MatcherAssert.assertThat("Driver should be on the provider realm page right now", + driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.providerRealmName() + "/")); + } + @Test public void testIdentityFirstIfUserNotExistsAndEmailMatchOrgDomain() { OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());