From 98a19e1a810d179fd8ab89b2c52d1dfa77acd8af Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Mon, 16 Dec 2024 11:47:56 -0300 Subject: [PATCH] Re-calculate the organization scope when re-authenticating in the browser flow Closes #35935 Closes #35830 Signed-off-by: Pedro Igor --- .../browser/CookieAuthenticator.java | 23 ++- .../browser/OrganizationAuthenticator.java | 56 +++-- .../oidc/OrganizationMembershipMapper.java | 21 +- .../OrganizationOIDCProtocolMapperTest.java | 194 +++++++++++++++--- 4 files changed, 244 insertions(+), 50 deletions(-) diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java index 12e36ca739ea..ed1f0ae1fba9 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java @@ -26,7 +26,10 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.organization.protocol.mappers.oidc.OrganizationScope; +import org.keycloak.organization.utils.Organizations; import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; @@ -84,7 +87,13 @@ public void authenticate(AuthenticationFlowContext context) { acrStore.setLevelAuthenticatedToCurrentRequest(previouslyAuthenticatedLevel); authSession.setAuthNote(AuthenticationManager.SSO_AUTH, "true"); context.attachUserSession(authResult.getSession()); - context.success(); + + if (isOrganizationContext(context)) { + // if re-authenticating in the scope of an organization, an organization must be resolved prior to authenticating the user + context.attempted(); + } else { + context.success(); + } } } } @@ -109,4 +118,16 @@ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserMo public void close() { } + + private boolean isOrganizationContext(AuthenticationFlowContext context) { + KeycloakSession session = context.getSession(); + + if (Organizations.isEnabledAndOrganizationsPresent(session)) { + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + String requestedScopes = authSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM); + return OrganizationScope.valueOfScope(requestedScopes) != null; + } + + return false; + } } diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java index 9fb8a968e739..180d835c84dd 100644 --- a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java @@ -17,6 +17,7 @@ package org.keycloak.organization.authentication.authenticators.browser; +import static org.keycloak.authentication.AuthenticatorUtil.isSSOAuthentication; import static org.keycloak.organization.utils.Organizations.getEmailDomain; import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent; import static org.keycloak.organization.utils.Organizations.resolveHomeBroker; @@ -121,7 +122,12 @@ public void action(AuthenticationFlowContext context) { return; } - context.attempted(); + if (isSSOAuthentication(context.getAuthenticationSession())) { + // if re-authenticating in the scope of an organization + context.success(); + } else { + context.attempted(); + } } @Override @@ -134,9 +140,17 @@ private OrganizationModel resolveOrganization(UserModel user, String domain) { HttpRequest request = context.getHttpRequest(); MultivaluedMap parameters = request.getDecodedFormParameters(); List alias = parameters.getOrDefault(OrganizationModel.ORGANIZATION_ATTRIBUTE, List.of()); + AuthenticationSessionModel authSession = context.getAuthenticationSession(); if (alias.isEmpty()) { - return Organizations.resolveOrganization(session, user, domain); + OrganizationModel organization = Organizations.resolveOrganization(session, user, domain); + + if (organization != null) { + // make sure the organization selected by the user is available from the client session when running mappers and issuing tokens + authSession.setClientNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); + } + + return organization; } OrganizationProvider provider = getOrganizationProvider(); @@ -146,7 +160,6 @@ private OrganizationModel resolveOrganization(UserModel user, String domain) { return null; } - AuthenticationSessionModel authSession = context.getAuthenticationSession(); // make sure the organization selected by the user is available from the client session when running mappers and issuing tokens authSession.setClientNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); @@ -274,20 +287,31 @@ private void unknownUserChallenge(AuthenticationFlowContext context, Organizatio context.challenge(form.createLoginUsername()); } - private void initialChallenge(AuthenticationFlowContext context){ - // the default challenge won't show any broker but just the identity-first login page and the option to try a different authentication mechanism - LoginFormsProvider form = context.form() - .setAttributeMapper(attributes -> { - attributes.computeIfPresent("social", - (key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, false, true) - ); - attributes.computeIfPresent("auth", - (key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false) - ); - return attributes; - }); + private void initialChallenge(AuthenticationFlowContext context) { + UserModel user = context.getUser(); - context.challenge(form.createLoginUsername()); + if (user == null) { + // the default challenge won't show any broker but just the identity-first login page and the option to try a different authentication mechanism + LoginFormsProvider form = context.form() + .setAttributeMapper(attributes -> { + attributes.computeIfPresent("social", + (key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, false, true) + ); + attributes.computeIfPresent("auth", + (key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false) + ); + return attributes; + }); + + context.challenge(form.createLoginUsername()); + } else if (isSSOAuthentication(context.getAuthenticationSession())) { + if (shouldUserSelectOrganization(context, user)) { + return; + } + + // user is re-authenticating and there are no organizations to select + context.success(); + } } private boolean hasPublicBrokers(OrganizationModel organization) { diff --git a/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java b/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java index 2c339899116e..3e5f1137c62a 100644 --- a/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java +++ b/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java @@ -110,18 +110,11 @@ public String getHelpText() { @Override protected void setClaim(IDToken token, ProtocolMapperModel model, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) { - String rawScopes = clientSessionCtx.getScopeString(); - OrganizationScope scope = OrganizationScope.valueOfScope(rawScopes); - - if (scope == null) { - return; - } - String orgId = clientSessionCtx.getClientSession().getNote(OrganizationModel.ORGANIZATION_ATTRIBUTE); Stream organizations; if (orgId == null) { - organizations = scope.resolveOrganizations(userSession.getUser(), rawScopes, session); + organizations = resolveFromRequestedScopes(session, userSession, clientSessionCtx); } else { organizations = Stream.of(session.getProvider(OrganizationProvider.class).getById(orgId)); } @@ -139,6 +132,18 @@ protected void setClaim(IDToken token, ProtocolMapperModel model, UserSessionMod OIDCAttributeMapperHelper.mapClaim(token, effectiveModel, claim); } + private Stream resolveFromRequestedScopes(KeycloakSession session, UserSessionModel userSession, ClientSessionContext context) { + String rawScopes = context.getScopeString(); + OrganizationScope scope = OrganizationScope.valueOfScope(rawScopes); + + if (scope == null) { + return Stream.empty(); + } + + return scope.resolveOrganizations(userSession.getUser(), rawScopes, session); + + } + private Object resolveValue(ProtocolMapperModel model, List organizations) { if (organizations.isEmpty()) { return null; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java index 4aaf5ad16cc8..1b39f9cf31c2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java @@ -39,6 +39,7 @@ import java.util.Map; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -68,7 +69,6 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration; import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest; -import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse; public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest { @@ -129,7 +129,6 @@ public void testMultipleOrganizationScopes() throws Exception { AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", memberEmail, memberPassword); Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode()); - oauth.scope("openid organization organization:*"); response = oauth.doGrantAccessTokenRequest("password", memberEmail, memberPassword); Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode()); @@ -178,13 +177,13 @@ public void testOrganizationScopeMapsSpecificOrganization() { String orgScope = "organization:" + orgB.getAlias(); oauth.scope(orgScope); loginPage.open(bc.consumerRealmName()); - org.keycloak.testsuite.Assert.assertFalse(loginPage.isPasswordInputPresent()); - org.keycloak.testsuite.Assert.assertTrue(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider")); - org.keycloak.testsuite.Assert.assertFalse(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider")); + assertFalse(loginPage.isPasswordInputPresent()); + assertTrue(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider")); + assertFalse(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider")); loginPage.loginUsername(member.getEmail()); - org.keycloak.testsuite.Assert.assertTrue(loginPage.isPasswordInputPresent()); - org.keycloak.testsuite.Assert.assertTrue(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider")); - org.keycloak.testsuite.Assert.assertFalse(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider")); + assertTrue(loginPage.isPasswordInputPresent()); + assertTrue(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider")); + assertFalse(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider")); loginPage.login(memberPassword); assertScopeAndClaims(orgScope, orgB); } @@ -200,9 +199,9 @@ public void testOrganizationScopeMapsAllOrganizations() { oauth.clientId("broker-app"); oauth.scope("organization:" + orgA.getAlias()); loginPage.open(bc.consumerRealmName()); - org.keycloak.testsuite.Assert.assertFalse(loginPage.isPasswordInputPresent()); - org.keycloak.testsuite.Assert.assertTrue(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider")); - org.keycloak.testsuite.Assert.assertFalse(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider")); + assertFalse(loginPage.isPasswordInputPresent()); + assertTrue(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider")); + assertFalse(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider")); // identity-first login will respect the organization provided in the scope even though the user email maps to a different organization oauth.clientId("broker-app"); @@ -211,8 +210,7 @@ public void testOrganizationScopeMapsAllOrganizations() { loginPage.open(bc.consumerRealmName()); loginPage.loginUsername(member.getEmail()); loginPage.login(memberPassword); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + AccessTokenResponse response = assertSuccessfulCodeGrant(); assertThat(response.getScope(), containsString(orgScope)); AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); assertThat(accessToken.getScope(), containsString(orgScope)); @@ -269,8 +267,7 @@ public void testOrganizationScopeAnyAskUserToSelectOrganization() { assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgB.getAlias())); selectOrganizationPage.selectOrganization(orgB.getAlias()); loginPage.login(memberPassword); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + AccessTokenResponse response = assertSuccessfulCodeGrant(); // for now, return the organization scope in the response and access token even though no organization is mapped into the token // once we support the user to select an organization, the selected organization will be mapped assertThat(response.getScope(), containsString("organization")); @@ -294,8 +291,7 @@ public void testRefreshTokenWithAllOrganizationsAskingForSpecificOrganization() loginPage.open(bc.consumerRealmName()); loginPage.loginUsername(member.getEmail()); loginPage.login(memberPassword); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + AccessTokenResponse response = assertSuccessfulCodeGrant(); assertThat(response.getScope(), containsString(orgScope)); AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); assertThat(accessToken.getScope(), containsString(orgScope)); @@ -328,8 +324,7 @@ public void testRefreshTokenWithSingleOrganizationsAskingAllOrganizations() { loginPage.open(bc.consumerRealmName()); loginPage.loginUsername(member.getEmail()); loginPage.login(memberPassword); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + AccessTokenResponse response = assertSuccessfulCodeGrant(); assertThat(response.getScope(), containsString(orgScope)); AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); assertThat(accessToken.getScope(), containsString(orgScope)); @@ -363,8 +358,7 @@ public void testRefreshTokenWithSingleOrganizationsAskingDifferentOrganization() loginPage.open(bc.consumerRealmName()); loginPage.loginUsername(member.getEmail()); loginPage.login(memberPassword); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + AccessTokenResponse response = assertSuccessfulCodeGrant(); assertThat(response.getScope(), containsString(orgScope)); AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); assertThat(accessToken.getScope(), containsString(orgScope)); @@ -398,8 +392,7 @@ public void testRefreshTokenScopeAnyAskingAllOrganizations() { assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgB.getAlias())); selectOrganizationPage.selectOrganization(orgB.getAlias()); loginPage.login(memberPassword); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + AccessTokenResponse response = assertSuccessfulCodeGrant(); // for now, return the organization scope in the response and access token even though no organization is mapped into the token // once we support the user to select an organization, the selected organization will be mapped assertThat(response.getScope(), containsString("organization")); @@ -434,8 +427,7 @@ public void testRefreshTokenScopeAnyAskingSingleOrganization() { assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgB.getAlias())); selectOrganizationPage.selectOrganization(orgB.getAlias()); loginPage.login(memberPassword); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + AccessTokenResponse response = assertSuccessfulCodeGrant(); assertThat(response.getScope(), containsString("organization")); AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); @@ -595,6 +587,158 @@ public void testAuthenticatingUsingBroker() { assertEquals(bc.getIDPAlias(), federatedIdentities.get(0).getIdentityProvider()); } + @Test + public void testMapDifferentOrganizationWhenReAuthenticating() { + OrganizationRepresentation orgA = createOrganization("orga", true); + MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName()); + OrganizationRepresentation orgB = createOrganization("orgb", true); + testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close(); + // identity-first login will respect the organization provided in the scope even though the user email maps to a different organization + oauth.clientId("broker-app"); + String originalScope = "organization:orga"; + String orgScope = originalScope; + oauth.scope(orgScope); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + loginPage.login(memberPassword); + AccessTokenResponse response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString(orgScope)); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getScope(), containsString(orgScope)); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + List organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.size(), is(1)); + assertThat(organizations.contains(orgA.getAlias()), is(true)); + orgScope = "organization:orgb"; + oauth.scope(orgScope); + oauth.realm(bc.consumerRealmName()); + oauth.openLoginForm(); + response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString(orgScope)); + accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getScope(), containsString(orgScope)); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.size(), is(1)); + assertThat(organizations.contains(orgB.getAlias()), is(true)); + } + + @Test + public void testSelectOrganizationMapDifferentOrganizationWhenReAuthenticating() { + OrganizationRepresentation orgA = createOrganization("orga", true); + MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName()); + OrganizationRepresentation orgB = createOrganization("orgb", true); + testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close(); + // identity-first login will respect the organization provided in the scope even though the user email maps to a different organization + oauth.clientId("broker-app"); + String originalScope = "organization"; + String orgScope = originalScope; + oauth.scope(orgScope); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + selectOrganizationPage.selectOrganization(orgA.getAlias()); + loginPage.login(memberPassword); + AccessTokenResponse response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString(orgScope)); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getScope(), containsString(orgScope)); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + List organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.size(), is(1)); + assertThat(organizations.contains(orgA.getAlias()), is(true)); + orgScope = "organization:orgb"; + oauth.scope(orgScope); + oauth.realm(bc.consumerRealmName()); + oauth.openLoginForm(); + response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString(orgScope)); + accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getScope(), containsString(orgScope)); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.size(), is(1)); + assertThat(organizations.contains(orgB.getAlias()), is(true)); + } + + @Test + public void testForceSelectingOrganizationWhenReAuthenticatingUsingDifferentClient() { + driver.manage().timeouts().pageLoadTimeout(Duration.ofDays(1)); + OrganizationRepresentation orgA = createOrganization("orga", true); + MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName()); + OrganizationRepresentation orgB = createOrganization("orgb", true); + testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close(); + ClientRepresentation client = testRealm().clients().findByClientId("broker-app").get(0); + client.setId(null); + client.setClientId("broker-app2"); + testRealm().clients().create(client).close(); + // identity-first login will respect the organization provided in the scope even though the user email maps to a different organization + oauth.clientId("broker-app"); + String originalScope = "organization:orga"; + String orgScope = originalScope; + oauth.scope(orgScope); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + loginPage.login(memberPassword); + AccessTokenResponse response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString(orgScope)); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getScope(), containsString(orgScope)); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + List organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.size(), is(1)); + assertThat(organizations.contains(orgA.getAlias()), is(true)); + orgScope = "organization"; + oauth.clientId("broker-app2"); + oauth.scope(orgScope); + oauth.realm(bc.consumerRealmName()); + oauth.openLoginForm(); + selectOrganizationPage.selectOrganization(orgB.getAlias()); + response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString(orgScope)); + accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getScope(), containsString(orgScope)); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.size(), is(1)); + assertThat(organizations.contains(orgB.getAlias()), is(true)); + } + + @Test + public void testReAuthenticationUserNotMemberOfOrganization() { + driver.manage().timeouts().pageLoadTimeout(Duration.ofDays(1)); + OrganizationRepresentation orgA = createOrganization("orga", true); + MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName()); + testRealm().organizations().get(orgA.getId()).members().member(member.getId()).delete().close(); + ClientRepresentation client = testRealm().clients().findByClientId("broker-app").get(0); + client.setId(null); + client.setClientId("broker-app2"); + testRealm().clients().create(client).close(); + // identity-first login will respect the organization provided in the scope even though the user email maps to a different organization + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + loginPage.login(memberPassword); + assertSuccessfulCodeGrant(); + oauth.openLoginForm(); + assertSuccessfulCodeGrant(); + oauth.scope("organization"); + oauth.openLoginForm(); + assertSuccessfulCodeGrant(); + oauth.scope("organization:" + orgA.getAlias()); + oauth.openLoginForm(); + assertSuccessfulCodeGrant(); + oauth.scope("organization:*"); + oauth.openLoginForm(); + assertSuccessfulCodeGrant(); + } + + private AccessTokenResponse assertSuccessfulCodeGrant() { + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + assertThat(Status.OK, is(Status.fromStatusCode(response.getStatusCode()))); + return response; + } + private ProtocolMapperRepresentation createGroupMapper() { ProtocolMapperRepresentation groupMapper = new ProtocolMapperRepresentation(); groupMapper.setName("groups");