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