Automatically redirect based on login hint

Closes #42715

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor
2025-09-26 15:42:16 -03:00
parent 94d428d450
commit faa0ccbb7d
3 changed files with 56 additions and 37 deletions

View File

@@ -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<String, String> 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 {

View File

@@ -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<UserRepresentation> 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);

View File

@@ -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());