mirror of
https://github.com/keycloak/keycloak.git
synced 2026-06-08 00:04:10 -04:00
Resolve organization when scope is requested and the user is a member or the email domain matches the organization
Closes #39864 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
88069cd5fb
commit
919554e6fc
4 changed files with 250 additions and 37 deletions
|
|
@ -215,19 +215,18 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -29,7 +30,6 @@ import java.util.Optional;
|
|||
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;
|
||||
|
|
@ -41,6 +41,7 @@ import org.keycloak.models.FederatedIdentityModel;
|
|||
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;
|
||||
|
|
@ -188,15 +189,18 @@ public class Organizations {
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
@ -205,51 +209,44 @@ public class Organizations {
|
|||
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();
|
||||
.toList();
|
||||
|
||||
if (organization.isPresent()) {
|
||||
return organization.get();
|
||||
if (organizations.size() == 1) {
|
||||
return organizations.get(0);
|
||||
}
|
||||
|
||||
if (user != null && domain == null) {
|
||||
domain = getEmailDomain(user);
|
||||
}
|
||||
|
||||
return ofNullable(domain)
|
||||
.map(provider::getByDomainName)
|
||||
.orElse(null);
|
||||
return resolveUserOrganization(organizations, user, domain)
|
||||
.orElseGet(() -> resolveOrganizationByDomain(user, domain, provider));
|
||||
}
|
||||
|
||||
public static OrganizationProvider getProvider(KeycloakSession session) {
|
||||
|
|
@ -282,15 +279,45 @@ public class Organizations {
|
|||
(!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(getEmailDomain(user)).orElse("");
|
||||
Stream<OrganizationDomainModel> domains = organization.getDomains();
|
||||
Stream<String> domainNames = domains.map(OrganizationDomainModel::getName);
|
||||
String emailDomain = Optional.ofNullable(domain).orElseGet(() -> getEmailDomain(user));
|
||||
|
||||
return organization.isMember(user) || domainNames.anyMatch(emailDomain::equals);
|
||||
if (emailDomain == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Stream<OrganizationDomainModel> domains = organization.getDomains();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -256,7 +256,11 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
|||
}
|
||||
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -301,6 +301,189 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
|
|||
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());
|
||||
|
|
|
|||
Loading…
Reference in a new issue