+
Skip to content

Resolve home organization when requesting the scope for all organizations #39977

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -215,19 +215,18 @@ private OrganizationModel resolveOrganization(UserModel user, String domain) {
}

private boolean shouldUserSelectOrganization(AuthenticationFlowContext context, UserModel user) {
OrganizationProvider provider = getOrganizationProvider();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
OrganizationScope scope = OrganizationScope.valueOfScope(session);

if (!OrganizationScope.ANY.equals(scope) || user == null) {
if (user == null || !OrganizationScope.ANY.equals(OrganizationScope.valueOfScope(session))) {
return false;
}

AuthenticationSessionModel authSession = context.getAuthenticationSession();

if (authSession.getClientNote(OrganizationModel.ORGANIZATION_ATTRIBUTE) != null) {
// organization already selected
return false;
}

OrganizationProvider provider = getOrganizationProvider();
Stream<OrganizationModel> organizations = provider.getByMember(user);

if (organizations.count() > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.keycloak.organization.utils;

import static java.util.Optional.of;
import static java.util.Optional.ofNullable;

import jakarta.ws.rs.core.MultivaluedMap;
Expand All @@ -29,7 +30,6 @@
import java.util.function.Consumer;
import java.util.stream.Stream;

import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionToken;
import org.keycloak.common.Profile;
Expand All @@ -41,6 +41,7 @@
import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupModel.Type;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel;
Expand Down Expand Up @@ -188,15 +189,18 @@ public static OrganizationModel resolveOrganization(KeycloakSession session, Use
}

public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user, String domain) {
if (!session.getContext().getRealm().isOrganizationsEnabled()) {
KeycloakContext context = session.getContext();
RealmModel realm = context.getRealm();

if (!realm.isOrganizationsEnabled()) {
return null;
}

Optional<OrganizationModel> organization = Optional.ofNullable(session.getContext().getOrganization());
OrganizationModel current = context.getOrganization();

if (organization.isPresent()) {
if (current != null) {
// resolved from current keycloak session
return organization.get();
return current;
}

OrganizationProvider provider = getProvider(session);
Expand All @@ -205,51 +209,44 @@ public static OrganizationModel resolveOrganization(KeycloakSession session, Use
return null;
}

AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
AuthenticationSessionModel authSession = context.getAuthenticationSession();

if (authSession != null) {
OrganizationScope scope = OrganizationScope.valueOfScope(session);

List<OrganizationModel> organizations = ofNullable(authSession.getAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE))
.map(provider::getById)
.map(List::of)
.orElseGet(() -> scope == null ? List.of() : scope.resolveOrganizations(user, session).toList());

if (organizations.size() == 1) {
// single organization mapped from authentication session
OrganizationModel resolved = organizations.get(0);
OrganizationModel organization = organizations.get(0);

if (user == null) {
return resolved;
return organization;
}

// make sure the user still maps to the organization from the authentication session
if (matchesOrganization(resolved, user)) {
return resolved;
if (organization.isMember(user) || matchesOrganizationDomain(organization, user, domain)) {
return organization;
}

return null;
} else if (scope != null && user != null) {
// organization scope requested but no user and no single organization mapped from the scope
return null;
return resolveUserOrganization(organizations, user, domain).orElse(null);
}
}

organization = ofNullable(user).stream().flatMap(provider::getByMember)
List<OrganizationModel> organizations = ofNullable(user).stream()
.flatMap(provider::getByMember)
.filter(OrganizationModel::isEnabled)
.findAny();

if (organization.isPresent()) {
return organization.get();
}
.toList();

if (user != null && domain == null) {
domain = getEmailDomain(user);
if (organizations.size() == 1) {
return organizations.get(0);
}

return ofNullable(domain)
.map(provider::getByDomainName)
.orElse(null);
return resolveUserOrganization(organizations, user, domain)
.orElseGet(() -> resolveOrganizationByDomain(user, domain, provider));
}

public static OrganizationProvider getProvider(KeycloakSession session) {
Expand Down Expand Up @@ -282,15 +279,45 @@ public static boolean isReadOnlyOrganizationMember(KeycloakSession session, User
(!organizationProvider.isEnabled() && org.isManaged(delegate)));
}

private static boolean matchesOrganization(OrganizationModel organization, UserModel user) {
if (organization == null || user == null) {
private static boolean matchesOrganizationDomain(OrganizationModel organization, UserModel user, String domain) {
if (organization == null) {
return false;
}

String emailDomain = Optional.ofNullable(domain).orElseGet(() -> getEmailDomain(user));

if (emailDomain == null) {
return false;
}

String emailDomain = Optional.ofNullable(getEmailDomain(user)).orElse("");
Stream<OrganizationDomainModel> domains = organization.getDomains();
Stream<String> domainNames = domains.map(OrganizationDomainModel::getName);

return organization.isMember(user) || domainNames.anyMatch(emailDomain::equals);
return domains.map(OrganizationDomainModel::getName).anyMatch(emailDomain::equals);
}

private static OrganizationModel resolveOrganizationByDomain(UserModel user, String domain, OrganizationProvider provider) {
if (user != null && domain == null) {
domain = getEmailDomain(user);
}

return ofNullable(domain)
.map(provider::getByDomainName)
.orElse(null);
}

private static Optional<OrganizationModel> resolveUserOrganization(List<OrganizationModel> organizations, UserModel user, String domain) {
OrganizationModel orgByDomain = null;

for (OrganizationModel o : organizations) {
if (o.isManaged(user)) {
return of(o);
}

if (matchesOrganizationDomain(o, user, domain)) {
orgByDomain = o;
}
}

return ofNullable(orgByDomain);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,11 @@ protected void assertIsMember(String userEmail, OrganizationResource organizatio
}

protected UserRepresentation getUserRepresentation(String userEmail) {
UsersResource users = adminClient.realm(bc.consumerRealmName()).users();
return getUserRepresentation(bc.consumerRealmName(), userEmail);
}

protected UserRepresentation getUserRepresentation(String realm, String userEmail) {
UsersResource users = adminClient.realm(realm).users();
List<UserRepresentation> reps = users.searchByEmail(userEmail, true);
Assert.assertFalse(reps.isEmpty());
Assert.assertEquals(1, reps.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,189 @@ public void testRedirectBrokerWhenManagedMember() {
assertIsMember(bc.getUserEmail(), organization);
}

@Test
public void testRedirectManagedMemberOfMultipleOrganizations() {
OrganizationResource orgA = testRealm().organizations().get(createOrganization("org-a").getId());
OrganizationResource orgB = testRealm().organizations().get(createOrganization("org-b").getId());
String email = bc.getUserLogin() + "@" + orgA.toRepresentation().getDomains().iterator().next().getName();
UserRepresentation account = UserBuilder.create()
.username(email)
.email(email)
.password(bc.getUserPassword())
.enabled(true)
.build();
UsersResource users = realmsResouce().realm(bc.providerRealmName()).users();
try (Response response = users.create(account)) {
account.setId(ApiUtil.getCreatedId(response));
}
UserRepresentation finalAccount = account;
getCleanup().addCleanup(() -> users.get(finalAccount.getId()).remove());
// add the member for the first time
assertBrokerRegistration(orgA, email, email);
account = getUserRepresentation(account.getEmail());
realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
realmsResouce().realm(bc.providerRealmName()).logoutAll();

orgB.members().addMember(account.getId()).close();
openIdentityFirstLoginPage(email, true, null, false, false);
// login to the organization identity provider using e-mail and automatically redirects to the app as the account already exists
loginPage.login(email, bc.getUserPassword());
appPage.assertCurrent();
UserRepresentation finalAccount1 = account;
getCleanup().addCleanup(() -> realmsResouce().realm(bc.consumerRealmName()).users().get(finalAccount1.getId()).remove());

realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
realmsResouce().realm(bc.providerRealmName()).logoutAll();
openIdentityFirstLoginPage(email, true, null, false, false);
// login to the organization identity provider user username and automatically redirects to the app as the account already exists
loginPage.login(account.getUsername(), bc.getUserPassword());
appPage.assertCurrent();
}

@Test
public void testRedirectManagedMemberOfMultipleOrganizationsAllOrganizationsScope() {
OrganizationResource orgA = testRealm().organizations().get(createOrganization("org-a").getId());
OrganizationResource orgB = testRealm().organizations().get(createOrganization("org-b").getId());
String email = bc.getUserLogin() + "@" + orgA.toRepresentation().getDomains().iterator().next().getName();
UserRepresentation account = UserBuilder.create()
.username(email)
.email(email)
.password(bc.getUserPassword())
.enabled(true)
.build();
UsersResource users = realmsResouce().realm(bc.providerRealmName()).users();
try (Response response = users.create(account)) {
account.setId(ApiUtil.getCreatedId(response));
}
UserRepresentation finalAccount = account;
getCleanup().addCleanup(() -> users.get(finalAccount.getId()).remove());
// add the member for the first time
assertBrokerRegistration(orgA, email, email);
account = getUserRepresentation(account.getEmail());
UserRepresentation finalAccount1 = account;
getCleanup().addCleanup(() -> realmsResouce().realm(bc.consumerRealmName()).users().get(finalAccount1.getId()).remove());
realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
realmsResouce().realm(bc.providerRealmName()).logoutAll();

orgB.members().addMember(account.getId()).close();
oauth.scope("organization:*");
openIdentityFirstLoginPage(email, true, null, false, false);
// login to the organization identity provider by username and automatically redirects to the app as the account already exists
loginPage.login(email, bc.getUserPassword());
appPage.assertCurrent();
}

@Test
public void testRedirectManagedMemberUsingUnManagedMemberAllOrganizationsScope() {
OrganizationResource orgA = testRealm().organizations().get(createOrganization("org-a").getId());
OrganizationResource orgB = testRealm().organizations().get(createOrganization("org-b").getId());
String email = bc.getUserLogin() + "@" + orgA.toRepresentation().getDomains().iterator().next().getName();
UserRepresentation account = UserBuilder.create()
.username(email)
.email(email)
.password(bc.getUserPassword())
.enabled(true)
.build();
UsersResource users = realmsResouce().realm(bc.providerRealmName()).users();
try (Response response = users.create(account)) {
account.setId(ApiUtil.getCreatedId(response));
}
UserRepresentation finalAccount = account;
getCleanup().addCleanup(() -> users.get(finalAccount.getId()).remove());
// add the member for the first time
assertBrokerRegistration(orgA, email, email);
account = getUserRepresentation(account.getEmail());
UserRepresentation finalAccount1 = account;
getCleanup().addCleanup(() -> realmsResouce().realm(bc.consumerRealmName()).users().get(finalAccount1.getId()).remove());
realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
realmsResouce().realm(bc.providerRealmName()).logoutAll();

orgB.members().addMember(account.getId()).close();
oauth.scope("organization:*");
String orgBEmail = bc.getUserLogin() + "@" + orgB.toRepresentation().getDomains().iterator().next().getName();
openIdentityFirstLoginPage(orgBEmail, true, null, false, false);
// login to the organization identity provider by username and as to review profile because the user is not yet linked with the idp
loginPage.login(email, bc.getUserPassword());
updateAccountInformationPage.assertCurrent();
// should enforce a email with the same domain as the organization
updateAccountInformationPage.updateAccountInformation(email, email, "f", "l");
Assert.assertTrue(driver.getPageSource().contains("Email domain does not match any domain from the organization"));

updateAccountInformationPage.updateAccountInformation(email, orgBEmail, "f", "l");
// user is asked to link accounts
idpConfirmLinkPage.assertCurrent();
}

@Test
public void testRedirectUnManagedMemberAllOrganizationsScope() {
OrganizationResource orgA = testRealm().organizations().get(createOrganization("org-a").getId());
OrganizationResource orgB = testRealm().organizations().get(createOrganization("org-b").getId());
String orgAEmail = bc.getUserLogin() + "@" + orgA.toRepresentation().getDomains().iterator().next().getName();
// create user without credential to force the redirect to the IdP
UserRepresentation account = UserBuilder.create()
.username(orgAEmail)
.email(orgAEmail)
.enabled(true)
.build();
UsersResource users = realmsResouce().realm(bc.consumerRealmName()).users();
try (Response response = users.create(account)) {
String id = ApiUtil.getCreatedId(response);
account.setId(id);
getCleanup().addCleanup(() -> users.get(id).remove());
}

// add the unmanaged member to both organizations
orgA.members().addMember(account.getId()).close();
orgB.members().addMember(account.getId()).close();

oauth.scope("organization:*");
// resolve both organizations and redirect the user automatically
openIdentityFirstLoginPage(orgAEmail, true, null, false, false);
assertTrue(driver.getPageSource().contains("Sign in to provider"));
openIdentityFirstLoginPage(bc.getUserLogin() + "@" + orgB.toRepresentation().getDomains().iterator().next().getName(), true, null, false, false);
assertTrue(driver.getPageSource().contains("Sign in to provider"));

oauth.scope("organization");
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
loginPage.loginUsername(orgAEmail);
selectOrganizationPage.assertCurrent();
}

@Test
public void testRedirectBrokerManagedMemberUsingUsernameAllOrganizationsScope() {
OrganizationResource orgA = testRealm().organizations().get(createOrganization("org-a").getId());
OrganizationResource orgB = testRealm().organizations().get(createOrganization("org-b").getId());
String email = bc.getUserLogin() + "@" + orgA.toRepresentation().getDomains().iterator().next().getName();
UserRepresentation account = UserBuilder.create()
.username(email)
.email(email)
.password(bc.getUserPassword())
.enabled(true)
.build();
UsersResource users = realmsResouce().realm(bc.providerRealmName()).users();
try (Response response = users.create(account)) {
account.setId(ApiUtil.getCreatedId(response));
}
UserRepresentation finalAccount = account;
getCleanup().addCleanup(() -> users.get(finalAccount.getId()).remove());
// add the member for the first time
assertBrokerRegistration(orgA, bc.getUserLogin(), email);
account = getUserRepresentation(account.getEmail());
UserRepresentation finalAccount1 = account;
getCleanup().addCleanup(() -> realmsResouce().realm(bc.consumerRealmName()).users().get(finalAccount1.getId()).remove());
realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
realmsResouce().realm(bc.providerRealmName()).logoutAll();

orgB.members().addMember(account.getId()).close();
oauth.scope("organization:*");
// provide the username, the user is automatically redirected to home broker because he is a managed member
openIdentityFirstLoginPage(bc.getUserLogin(), true, null, false, false);
// login to the organization identity provider by username
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
appPage.assertCurrent();
}

@Test
public void testRedirectBrokerWhenUnmanagedMemberProfileEmailMatchesOrganization() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载