mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-30 03:19:54 -06:00
Automatically redirect based on login hint
Closes #42715 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user