diff --git a/docs/UAA-Configuration-Reference.md b/docs/UAA-Configuration-Reference.md index 757cc032a5d..de3cb4c17a4 100644 --- a/docs/UAA-Configuration-Reference.md +++ b/docs/UAA-Configuration-Reference.md @@ -204,6 +204,7 @@ or `$CLOUDFOUNDRY_CONFIG_PATH/uaa.yml`. | `login.allowOriginLoop` | `true`| Allow origin loop| | `login.aliasEntitiesEnabled` | `false`| Enable alias entities| | `login.oauth.providers` | —| External OAuth/OIDC providers| +| `login.oauth.externalGroupsFromMappedAuthorities` | `false`| Use mapped UAA authorities for external OAuth external groups| ### SAML Service Provider @@ -1866,6 +1867,18 @@ External OAuth 2.0 and OIDC provider definitions. Each provider entry includes: --- +### `login.oauth.externalGroupsFromMappedAuthorities` + +**Default:** `false` +**Source:** `@Value("${login.oauth.externalGroupsFromMappedAuthorities:false}")` in [`OauthEndpointBeanConfiguration`](../server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java) (bean `externalOAuthAuthenticationManager`) +**Type:** `boolean` + +When `false` (default), external OAuth login populates `UaaAuthentication` external group names from IdP token authorities **before** UAA group mapping (`externalAuthorities`). When `true`, external group names are taken from **mapped** UAA authorities (`authorities` after `mapExternalGroups`), for deployments that want downstream external-group membership logic to follow mapped group or scope names instead of raw IdP values. + +[Back to table](#login--branding) + +--- + ### `login.saml.activeKeyId` **Default:** — (none) diff --git a/scripts/boot/uaa.yml b/scripts/boot/uaa.yml index 9409809259b..6bf8be76eb5 100644 --- a/scripts/boot/uaa.yml +++ b/scripts/boot/uaa.yml @@ -63,6 +63,9 @@ login: # The entity base url is the location of this application (Used for SAML SP metadata generation) # Not set for integration tests - allows UAA to use request URL for zone subdomains entityBaseURL: null + # Non-default: integration server exercises mapped-authorities path for external OAuth external groups + oauth: + externalGroupsFromMappedAuthorities: true saml: activeKeyId: key1 keys: diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java index 1a41869a065..079a9cf003a 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java @@ -555,7 +555,8 @@ ExternalOAuthAuthenticationManager externalOAuthAuthenticationManager( @Qualifier("keyInfoService") KeyInfoService keyInfoService, @Qualifier("oidcMetadataFetcher") OidcMetadataFetcher oidcMetadataFetcher, @Qualifier("userDatabase") UaaUserDatabase userDatabase, - @Qualifier("externalGroupMembershipManager") ScimGroupExternalMembershipManager externalMembershipManager + @Qualifier("externalGroupMembershipManager") ScimGroupExternalMembershipManager externalMembershipManager, + @Value("${login.oauth.externalGroupsFromMappedAuthorities:false}") boolean externalGroupsFromMappedAuthorities ) { ExternalOAuthAuthenticationManager bean = new ExternalOAuthAuthenticationManager( providerProvisioning, @@ -564,7 +565,8 @@ ExternalOAuthAuthenticationManager externalOAuthAuthenticationManager( nonTrustingRestTemplate, tokenEndpointBuilder, keyInfoService, - oidcMetadataFetcher + oidcMetadataFetcher, + externalGroupsFromMappedAuthorities ); bean.setUserDatabase(userDatabase); bean.setExternalMembershipManager(externalMembershipManager); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java index c0933008e5b..7706380db2f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java @@ -148,6 +148,7 @@ public class ExternalOAuthAuthenticationManager extends ExternalLoginAuthenticat @Getter private final KeyInfoService keyInfoService; private final IdentityZoneManager identityZoneManager; + private final boolean externalGroupsFromMappedAuthorities; public ExternalOAuthAuthenticationManager( IdentityProviderProvisioning providerProvisioning, @@ -156,7 +157,8 @@ public ExternalOAuthAuthenticationManager( RestTemplate nonTrustingRestTemplate, TokenEndpointBuilder tokenEndpointBuilder, KeyInfoService keyInfoService, - OidcMetadataFetcher oidcMetadataFetcher + OidcMetadataFetcher oidcMetadataFetcher, + boolean externalGroupsFromMappedAuthorities ) { super(providerProvisioning); this.identityZoneManager = identityZoneManager; @@ -165,6 +167,7 @@ public ExternalOAuthAuthenticationManager( this.tokenEndpointBuilder = tokenEndpointBuilder; this.keyInfoService = keyInfoService; this.oidcMetadataFetcher = oidcMetadataFetcher; + this.externalGroupsFromMappedAuthorities = externalGroupsFromMappedAuthorities; } /** @@ -393,8 +396,11 @@ protected void populateAuthenticationAttributes(UaaAuthentication authentication } authentication.setUserAttributes(userAttributes); + List authoritiesForExternalGroups = externalGroupsFromMappedAuthorities + ? authenticationData.getAuthorities() + : authenticationData.getExternalAuthorities(); authentication.setExternalGroups( - Optional.ofNullable(authenticationData.getExternalAuthorities()) + Optional.ofNullable(authoritiesForExternalGroups) .orElse(emptyList()) .stream() .map(GrantedAuthority::getAuthority) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerGithubTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerGithubTest.java index 99976067ed5..1f20f6d4f72 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerGithubTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerGithubTest.java @@ -92,7 +92,7 @@ void beforeEach() throws Exception { nonTrustingRestTemplate ); authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), trustingRestTemplate, - nonTrustingRestTemplate, tokenEndpointBuilder, new KeyInfoService(uaaIssuerBaseUrl), oidcMetadataFetcher); + nonTrustingRestTemplate, tokenEndpointBuilder, new KeyInfoService(uaaIssuerBaseUrl), oidcMetadataFetcher, false); } @AfterEach diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java index 52444bfa6a2..237145a686a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerIT.java @@ -230,7 +230,7 @@ void setUp() throws Exception { identityZoneProvisioning, identityZoneManager) ); - externalOAuthAuthenticationManager = spy(new ExternalOAuthAuthenticationManager(externalOAuthProviderConfigurator, identityZoneManager, trustingRestTemplate, nonTrustingRestTemplate, tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_URL), oidcMetadataFetcher)); + externalOAuthAuthenticationManager = spy(new ExternalOAuthAuthenticationManager(externalOAuthProviderConfigurator, identityZoneManager, trustingRestTemplate, nonTrustingRestTemplate, tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_URL), oidcMetadataFetcher, false)); externalOAuthAuthenticationManager.setUserDatabase(userDatabase); externalOAuthAuthenticationManager.setExternalMembershipManager(externalMembershipManager); externalOAuthAuthenticationManager.setApplicationEventPublisher(publisher); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java index 4fe49ca89dd..f31894bd580 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManagerTest.java @@ -220,7 +220,7 @@ void beforeEach() throws Exception { new RestTemplate(), new RestTemplate() ); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), new RestTemplate(), new RestTemplate(), tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_BASE_URL), oidcMetadataFetcher); + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), new RestTemplate(), new RestTemplate(), tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_BASE_URL), oidcMetadataFetcher, false); authManager.setExternalMembershipManager(externalMembershipManager); authManager.setUserDatabase(userDatabase); } @@ -606,7 +606,7 @@ void getUser_doesThrowWhenIdTokenMappingIsWrongType() { } @Test - void populateAuthenticationAttributes_setsIdpIdTokenAndExternalGroups() { + void populateAuthenticationAttributes_setsIdpIdTokenAndExternalGroupsFromExternalAuthoritiesByDefault() { UaaAuthentication authentication = new UaaAuthentication(new UaaPrincipal("user-guid", "marissa", "marissa@test.org", "uaa", "", ""), Collections.emptyList(), null); Map header = map( entry(HeaderParameterNames.ALGORITHM, JWSAlgorithm.RS256.getName()), @@ -627,10 +627,44 @@ void populateAuthenticationAttributes_setsIdpIdTokenAndExternalGroups() { String idTokenJwt = UaaTokenUtils.constructToken(header, claims, signer); ExternalOAuthCodeToken oidcAuthentication = new ExternalOAuthCodeToken(null, ORIGIN, "http://google.com", idTokenJwt, "accesstoken", "signedrequest"); ExternalOAuthAuthenticationManager.AuthenticationData authenticationData = authManager.getExternalAuthenticationDetails(oidcAuthentication); - authenticationData.setExternalAuthorities(List.of(new SimpleGrantedAuthority("uaa-authorities"))); + authenticationData.setAuthorities(List.of(new SimpleGrantedAuthority("uaa-authorities"))); + authenticationData.setExternalAuthorities(List.of(new SimpleGrantedAuthority("raw_external_group"))); authManager.populateAuthenticationAttributes(authentication, oidcAuthentication, authenticationData); assertThat(authentication.getIdpIdToken()).isEqualTo(idTokenJwt); - assertThat(authentication.getExternalGroups()).containsAll(List.of("uaa-authorities")); + assertThat(authentication.getExternalGroups()).containsExactlyInAnyOrder("raw_external_group"); + } + + @Test + void populateAuthenticationAttributes_setsExternalGroupsFromMappedAuthoritiesWhenEnabled() { + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), new RestTemplate(), new RestTemplate(), tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_BASE_URL), oidcMetadataFetcher, true); + authManager.setExternalMembershipManager(externalMembershipManager); + authManager.setUserDatabase(userDatabase); + + UaaAuthentication authentication = new UaaAuthentication(new UaaPrincipal("user-guid", "marissa", "marissa@test.org", "uaa", "", ""), Collections.emptyList(), null); + Map header = map( + entry(HeaderParameterNames.ALGORITHM, JWSAlgorithm.RS256.getName()), + entry(HeaderParameterNames.KEY_ID, OIDC_PROVIDER_KEY) + ); + JWSSigner signer = new KeyInfo("uaa-key", OIDC_PROVIDER_TOKEN_SIGNING_KEY, DEFAULT_UAA_URL).getSigner(); + Map entryMap = map( + entry("external_map_name", Arrays.asList("bar", "baz")) + ); + Map claims = map( + entry("external_family_name", entryMap), + entry(ISS, oidcConfig.getIssuer()), + entry(AUD, "uaa-relying-party"), + entry(EXPIRY_IN_SECONDS, ((int) (System.currentTimeMillis() / 1000L)) + 60), + entry(SUB, "abc-def-asdf") + ); + IdentityZoneHolder.get().getConfig().getTokenPolicy().setKeys(Collections.singletonMap("uaa-key", UAA_IDENTITY_ZONE_TOKEN_SIGNING_KEY)); + String idTokenJwt = UaaTokenUtils.constructToken(header, claims, signer); + ExternalOAuthCodeToken oidcAuthentication = new ExternalOAuthCodeToken(null, ORIGIN, "http://google.com", idTokenJwt, "accesstoken", "signedrequest"); + ExternalOAuthAuthenticationManager.AuthenticationData authenticationData = authManager.getExternalAuthenticationDetails(oidcAuthentication); + authenticationData.setAuthorities(List.of(new SimpleGrantedAuthority("uaa-authorities"))); + authenticationData.setExternalAuthorities(List.of(new SimpleGrantedAuthority("raw_external_group"))); + authManager.populateAuthenticationAttributes(authentication, oidcAuthentication, authenticationData); + assertThat(authentication.getIdpIdToken()).isEqualTo(idTokenJwt); + assertThat(authentication.getExternalGroups()).containsExactlyInAnyOrder("uaa-authorities"); } @Test @@ -653,7 +687,7 @@ void getClaimsFromToken_setsIdToken() { String idTokenJwt = UaaTokenUtils.constructToken(header, claims, signer); ExternalOAuthCodeToken codeToken = new ExternalOAuthCodeToken("thecode", ORIGIN, "http://google.com", null, "accesstoken", "signedrequest"); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), new RestTemplate(), new RestTemplate(), tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_BASE_URL), null) { + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), new RestTemplate(), new RestTemplate(), tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_BASE_URL), null, false) { @Override protected > String getTokenFromCode( ExternalOAuthCodeToken codeToken, @@ -674,7 +708,7 @@ protected > String void fetchOidcMetadata() throws OidcMetadataFetchingException { OIDCIdentityProviderDefinition mockedProviderDefinition = mock(OIDCIdentityProviderDefinition.class); OidcMetadataFetcher mockedOidcMetadataFetcher = mock(OidcMetadataFetcher.class); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), new RestTemplate(), new RestTemplate(), tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_BASE_URL), mockedOidcMetadataFetcher); + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), new RestTemplate(), new RestTemplate(), tokenEndpointBuilder, new KeyInfoService(UAA_ISSUER_BASE_URL), mockedOidcMetadataFetcher, false); doThrow(new OidcMetadataFetchingException("error")).when(mockedOidcMetadataFetcher).fetchMetadataAndUpdateDefinition(mockedProviderDefinition); assertThatNoException().isThrownBy(() -> authManager.fetchMetadataAndUpdateDefinition(mockedProviderDefinition)); } @@ -829,7 +863,7 @@ void oidcPasswordGrantProviderJwtClientCredentials() throws ParseException, JOSE when(rt.exchange(eq("http://localhost:8080/uaa/oauth/token"), eq(HttpMethod.POST), any(HttpEntity.class), any(ParameterizedTypeReference.class))).thenReturn(responseEntity); when(responseEntity.hasBody()).thenReturn(true); when(responseEntity.getBody()).thenReturn(Map.of("id_token", "dummy")); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher); + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher, false); // When authManager.oauthTokenRequest(null, mockOidcIdentityProvider(), GRANT_TYPE_PASSWORD, new LinkedMaskingMultiValueMap<>()); @@ -866,7 +900,7 @@ void oidcJwtBearerProviderJwtClientCredentials() throws ParseException, JOSEExce when(rt.exchange(eq("http://localhost:8080/uaa/oauth/token"), eq(HttpMethod.POST), any(HttpEntity.class), any(ParameterizedTypeReference.class))).thenReturn(responseEntity); when(responseEntity.hasBody()).thenReturn(true); when(responseEntity.getBody()).thenReturn(Map.of("id_token", "dummy")); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher); + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher, false); // When assertThat(authManager.oidcJwtBearerGrant(uaaAuthenticationDetails, mockOidcIdentityProvider() , "proxy-token")).isEqualTo("dummy"); @@ -906,7 +940,7 @@ void oidcJwtBearerProviderProxyThrowException() throws JOSEException, MalformedU when(config.getRelyingPartySecret()).thenReturn("secret"); doReturn(false).when(config).isClientAuthInBody(); when(rt.exchange(eq("http://localhost:8080/uaa/oauth/token"), eq(HttpMethod.POST), any(HttpEntity.class), any(ParameterizedTypeReference.class))).thenThrow(new HttpClientErrorException(HttpStatus.UNAUTHORIZED)); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher); + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher, false); // When assertThatThrownBy(() -> authManager.oidcJwtBearerGrant(uaaAuthenticationDetails, identityProvider, "proxy-token")) @@ -939,7 +973,7 @@ void oidcPasswordGrantWithForwardHeader() throws JOSEException, MalformedURLExce when(auth.getDetails()).thenReturn(details); RestTemplate rt = mock(RestTemplate.class); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher); + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher, false); ResponseEntity> response = mock(ResponseEntity.class); when(response.hasBody()).thenReturn(true); @@ -992,7 +1026,7 @@ void oidcPasswordGrant_credentialsMustBeStringButNoSecretNeeded() throws Malform when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), any(ParameterizedTypeReference.class))).thenReturn(responseEntity); when(responseEntity.hasBody()).thenReturn(true); when(responseEntity.getBody()).thenReturn(Map.of("id_token", "dummy")); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), restTemplate, restTemplate, tokenEndpointBuilder, mockKeyInfoService(), oidcMetadataFetcher); + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), restTemplate, restTemplate, tokenEndpointBuilder, mockKeyInfoService(), oidcMetadataFetcher, false); final IdentityProvider localIdp = new IdentityProvider<>(); localIdp.setOriginKey(new AlphanumericRandomValueStringGenerator(8).generate().toLowerCase()); @@ -1044,7 +1078,7 @@ void oidcPasswordGrantWithPrompts() throws MalformedURLException, JOSEException when(config.getScopes()).thenReturn(null); RestTemplate rt = mock(RestTemplate.class); - authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher); + authManager = new ExternalOAuthAuthenticationManager(identityProviderProvisioning, new IdentityZoneManagerImpl(), rt, rt, tokenEndpointBuilder, keyInfoService, oidcMetadataFetcher, false); ResponseEntity> response = mock(ResponseEntity.class); when(response.hasBody()).thenReturn(true); @@ -1102,7 +1136,8 @@ void oauth20AuthorizationFlowWithUserInfo() throws Exception { restTemplate, tokenEndpointBuilder, new KeyInfoService("http://uaa.example.com"), - null + null, + false ) { @Override protected > String getTokenFromCode( diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OIDCLoginIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OIDCLoginIT.java index 12bd9380a40..e8b80955a0c 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OIDCLoginIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OIDCLoginIT.java @@ -14,6 +14,8 @@ package org.cloudfoundry.identity.uaa.integration.feature; import com.fasterxml.jackson.core.type.TypeReference; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; import org.cloudfoundry.identity.uaa.ServerRunningExtension; import org.cloudfoundry.identity.uaa.account.UserInfoResponse; import org.cloudfoundry.identity.uaa.client.UaaClientDetails; @@ -68,18 +70,23 @@ import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.cloudfoundry.identity.uaa.integration.util.IntegrationTestUtils.isMember; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.SUB; import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_AUTHORIZATION_CODE; +import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_PASSWORD; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME; @SpringJUnitConfig(classes = DefaultIntegrationTestConfig.class) @@ -435,6 +442,42 @@ void shadowUserNameDefaultsToOIDCSubjectClaim() { assertThat(shadowUser.getUserName()).isEqualTo(expectedUsername); } + @Test + void roleMappingAndUserAttributesFromIdTokenOfZone() throws ParseException { + // test role and user_attribute claims in id token with external group membership assignment, see issue https://github.com/cloudfoundry/uaa/issues/3813 + Map attributeMappings = new HashMap<>(identityProvider.getConfig().getAttributeMappings()); + attributeMappings.remove(USER_NAME_ATTRIBUTE_NAME); + attributeMappings.put("user.attribute.roles", "scope"); + if (identityProvider.getConfig() instanceof OIDCIdentityProviderDefinition oidcConfig) { + oidcConfig.setStoreCustomAttributes(true); + oidcConfig.setPasswordGrantEnabled(true); + oidcConfig.setAttributeMappings(attributeMappings); + } + updateProvider(); + + String clientId = "client" + new RandomValueStringGenerator(5).generate(); + UaaClientDetails passwordClient = new UaaClientDetails(clientId, null, "openid,roles,user_attributes,"+createdGroup.getDisplayName(), GRANT_TYPE_PASSWORD, "uaa.none", baseUrl); + passwordClient.setClientSecret("clientsecret"); + passwordClient.setAutoApproveScopes(Collections.singletonList("true")); + IntegrationTestUtils.createClientAsZoneAdmin(clientCredentialsToken, baseUrl, zone.getId(), passwordClient); + Map response = IntegrationTestUtils.getPasswordToken(zoneUrl, passwordClient.getClientId(), "clientsecret", testAccounts.getUserName(), testAccounts.getPassword(), null, + identityProvider.getOriginKey()); + assertThat(response).isNotNull(); + String idToken = response.get("id_token") instanceof String idString ? idString : null; + assertThat(idToken).isNotNull(); + + JWTClaimsSet jwtClaimsSet = JWTParser.parse(idToken).getJWTClaimsSet(); + assertThat(jwtClaimsSet.getStringClaim("origin")).isEqualTo(identityProvider.getOriginKey()); + Set rolesInJwt = Arrays.stream(jwtClaimsSet.getStringArrayClaim("roles")).collect(Collectors.toSet()); + assertThat(rolesInJwt).isNotNull().contains(createdGroup.getDisplayName()); + Map userAttributeJwt = jwtClaimsSet.getJSONObjectClaim("user_attributes"); + assertThat(userAttributeJwt).isInstanceOf(Map.class); + List attr1 = userAttributeJwt.get("the_client_id") instanceof ArrayList arrayList ? arrayList : null; + assertThat(attr1).isNotNull().contains("identity"); + List attr2 = userAttributeJwt.get("roles") instanceof ArrayList arrayList ? arrayList : null; + assertThat(attr2).isNotNull().contains("openid"); + } + @Test void claimsComeFromUserInfoEndpoint() { AbstractExternalOAuthIdentityProviderDefinition oldConfig = identityProvider.getConfig(); @@ -442,6 +485,7 @@ void claimsComeFromUserInfoEndpoint() { attributeMappings.remove(USER_NAME_ATTRIBUTE_NAME); oldConfig.setAttributeMappings(attributeMappings); oldConfig.setLinkText("My Oauth2.0 Provider"); + oldConfig.setStoreCustomAttributes(true); //change the type so that we will use the /userinfo endpoint identityProvider.setType(OriginKeys.OAUTH20); updateProvider(); @@ -461,7 +505,7 @@ void claimsComeFromUserInfoEndpoint() { localhostServerRunning.setHostName("localhost"); String clientId = "client" + new RandomValueStringGenerator(5).generate(); - UaaClientDetails client = new UaaClientDetails(clientId, null, "openid", GRANT_TYPE_AUTHORIZATION_CODE, "openid", baseUrl); + UaaClientDetails client = new UaaClientDetails(clientId, null, "openid,roles,user_attributes,"+createdGroup.getDisplayName(), GRANT_TYPE_AUTHORIZATION_CODE, "openid", baseUrl); client.setClientSecret("clientsecret"); client.setAutoApproveScopes(Collections.singletonList("true")); IntegrationTestUtils.createClient(adminToken, baseUrl, client); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java index b50f7547871..1bbc40ca0c7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java @@ -1127,12 +1127,22 @@ public static String getClientCredentialsToken(String baseUrl, return accessToken.getValue(); } + public static Map getPasswordToken(String baseUrl, + String clientId, + String clientSecret, + String username, + String password, + String scopes) { + return getPasswordToken(baseUrl, clientId, clientSecret, username, password, scopes, null); + } + public static Map getPasswordToken(String baseUrl, String clientId, String clientSecret, String username, String password, - String scopes) { + String scopes, + String loginHint) { RestTemplate template = new RestTemplate(); template.getMessageConverters().addFirst(new StringHttpMessageConverter(StandardCharsets.UTF_8)); template.setRequestFactory(new StatelessRequestFactory()); @@ -1145,6 +1155,9 @@ public static Map getPasswordToken(String baseUrl, if (hasText(scopes)) { formData.add("scope", scopes); } + if (loginHint != null) { + formData.add("login_hint", "{\"origin\": \""+loginHint+"\"}"); + } HttpHeaders headers = new HttpHeaders(); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenExchangeOverrideAuthManagerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenExchangeOverrideAuthManagerMockMvcTests.java index 4e2a85c7485..ea124da2a27 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenExchangeOverrideAuthManagerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenExchangeOverrideAuthManagerMockMvcTests.java @@ -59,7 +59,8 @@ ExternalOAuthAuthenticationManager tokenExchangeAuthenticationManager( nonTrustingRestTemplate, tokenEndpointBuilder, keyInfoService, - oidcMetadataFetcher + oidcMetadataFetcher, + false ) { @Override public AuthenticationData getExternalAuthenticationDetails(Authentication authentication) {