Resolve first the user by username and fallback to the email during the identity-first login flow

Closes #38852

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2025-04-15 18:07:20 -03:00 committed by GitHub
parent cf6c8b07c5
commit b9d38d0fe9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 62 additions and 4 deletions

View File

@ -19,6 +19,7 @@ package org.keycloak.organization.authentication.authenticators.browser;
import static org.keycloak.authentication.AuthenticatorUtil.isSSOAuthentication;
import static org.keycloak.models.OrganizationDomainModel.ANY_DOMAIN;
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;
@ -33,7 +34,6 @@ import java.util.stream.Stream;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator;
@ -51,8 +51,8 @@ import org.keycloak.models.OrganizationModel;
import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareAuthenticationContextBean;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean;
@ -280,9 +280,8 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return null;
}
UserProvider users = session.users();
RealmModel realm = session.getContext().getRealm();
UserModel user = Optional.ofNullable(users.getUserByEmail(realm, username)).orElseGet(() -> users.getUserByUsername(realm, username));
UserModel user = findUserByNameOrEmail(session, realm, username);
// make sure the organization will be resolved based on the username provided
clearAuthenticationSession(context);

View File

@ -26,6 +26,7 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import jakarta.ws.rs.core.Response;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Test;
@ -35,12 +36,15 @@ import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.organization.authentication.authenticators.browser.OrganizationAuthenticatorFactory;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.util.UserBuilder;
public class OrganizationAuthenticationTest extends AbstractOrganizationTest {
@ -209,6 +213,61 @@ public class OrganizationAuthenticationTest extends AbstractOrganizationTest {
appPage.assertCurrent();
}
@Test
public void testDuplicateEmailsEnabled() {
RealmRepresentation realm = testRealm().toRepresentation();
realm.setDuplicateEmailsAllowed(true);
realm.setLoginWithEmailAllowed(false);
realm.setRegistrationEmailAsUsername(false);
testRealm().update(realm);
OrganizationRepresentation organization = createOrganization();
OrganizationResource organizationResource = testRealm().organizations().get(organization.getId());
UserRepresentation member = addMember(organizationResource);
UserRepresentation duplicatedUser = UserBuilder.create()
.username("duplicated-user")
.password("duplicated-user")
.email(member.getEmail())
.enabled(true).build();
try (Response response = testRealm().users().create(duplicatedUser)) {
duplicatedUser.setId(ApiUtil.getCreatedId(response));
}
// user with a unique username can authenticate to his account using a unique username
oauth.clientId("broker-app");
oauth.realm(bc.consumerRealmName());
oauth.loginForm().open();
loginPage.loginUsername(member.getUsername());
loginPage.clickSignIn();
loginPage.login(memberPassword);
appPage.assertCurrent();
testRealm().users().get(member.getId()).logout();
// a different account with the same email can also authenticate using a unique username
oauth.loginForm().open();
loginPage.loginUsername(duplicatedUser.getUsername());
loginPage.clickSignIn();
loginPage.login(duplicatedUser.getUsername());
appPage.assertCurrent();
testRealm().users().get(duplicatedUser.getId()).logout();
// trying to authenticate with the duplicated user using the email will fail because the username is the email of a different account
oauth.loginForm().open();
loginPage.loginUsername(duplicatedUser.getEmail());
loginPage.clickSignIn();
loginPage.login(duplicatedUser.getEmail());
assertThat(loginPage.getInputError(), is("Invalid password."));
// trying to authenticate to the account that has the email as username is ok
oauth.loginForm().open();
loginPage.loginUsername(member.getEmail());
loginPage.clickSignIn();
loginPage.login(memberPassword);
appPage.assertCurrent();
}
private void runOnServer(RunOnServer function) {
testingClient.server(bc.consumerRealmName()).run(function);
}