Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/UAA-Configuration-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ or `$CLOUDFOUNDRY_CONFIG_PATH/uaa.yml`.
| <a href="#loginalloworiginloop"><img src="images/click-me.png" width="14" height="14"/></a> `login.allowOriginLoop` | `true`| Allow origin loop|
| <a href="#loginaliasEntitiesenabled"><img src="images/click-me.png" width="14" height="14"/></a> `login.aliasEntitiesEnabled` | `false`| Enable alias entities|
| <a href="#loginoauthproviders"><img src="images/click-me.png" width="14" height="14"/></a> `login.oauth.providers` | —| External OAuth/OIDC providers|
| <a href="#loginoauthexternalgroupsfrommappedauthorities"><img src="images/click-me.png" width="14" height="14"/></a> `login.oauth.externalGroupsFromMappedAuthorities` | `false`| Use mapped UAA authorities for external OAuth external groups|

### SAML Service Provider

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions scripts/boot/uaa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -564,7 +565,8 @@ ExternalOAuthAuthenticationManager externalOAuthAuthenticationManager(
nonTrustingRestTemplate,
tokenEndpointBuilder,
keyInfoService,
oidcMetadataFetcher
oidcMetadataFetcher,
externalGroupsFromMappedAuthorities
);
bean.setUserDatabase(userDatabase);
bean.setExternalMembershipManager(externalMembershipManager);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -156,7 +157,8 @@ public ExternalOAuthAuthenticationManager(
RestTemplate nonTrustingRestTemplate,
TokenEndpointBuilder tokenEndpointBuilder,
KeyInfoService keyInfoService,
OidcMetadataFetcher oidcMetadataFetcher
OidcMetadataFetcher oidcMetadataFetcher,
boolean externalGroupsFromMappedAuthorities
) {
super(providerProvisioning);
this.identityZoneManager = identityZoneManager;
Expand All @@ -165,6 +167,7 @@ public ExternalOAuthAuthenticationManager(
this.tokenEndpointBuilder = tokenEndpointBuilder;
this.keyInfoService = keyInfoService;
this.oidcMetadataFetcher = oidcMetadataFetcher;
this.externalGroupsFromMappedAuthorities = externalGroupsFromMappedAuthorities;
}

/**
Expand Down Expand Up @@ -393,8 +396,11 @@ protected void populateAuthenticationAttributes(UaaAuthentication authentication
}
authentication.setUserAttributes(userAttributes);

List<SimpleGrantedAuthority> authoritiesForExternalGroups = externalGroupsFromMappedAuthorities
? authenticationData.getAuthorities()
: authenticationData.getExternalAuthorities();
authentication.setExternalGroups(
Optional.ofNullable(authenticationData.getExternalAuthorities())
Optional.ofNullable(authoritiesForExternalGroups)
.orElse(emptyList())
.stream()
.map(GrantedAuthority::getAuthority)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<String, Object> header = map(
entry(HeaderParameterNames.ALGORITHM, JWSAlgorithm.RS256.getName()),
Expand All @@ -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<String, Object> 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<String, Object> entryMap = map(
entry("external_map_name", Arrays.asList("bar", "baz"))
);
Map<String, Object> 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
Expand All @@ -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 <T extends AbstractExternalOAuthIdentityProviderDefinition<T>> String getTokenFromCode(
ExternalOAuthCodeToken codeToken,
Expand All @@ -674,7 +708,7 @@ protected <T extends AbstractExternalOAuthIdentityProviderDefinition<T>> 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));
}
Expand Down Expand Up @@ -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<>());
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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<Map<String, String>> response = mock(ResponseEntity.class);
when(response.hasBody()).thenReturn(true);
Expand Down Expand Up @@ -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<OIDCIdentityProviderDefinition> localIdp = new IdentityProvider<>();
localIdp.setOriginKey(new AlphanumericRandomValueStringGenerator(8).generate().toLowerCase());
Expand Down Expand Up @@ -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<Map<String, String>> response = mock(ResponseEntity.class);
when(response.hasBody()).thenReturn(true);
Expand Down Expand Up @@ -1102,7 +1136,8 @@ void oauth20AuthorizationFlowWithUserInfo() throws Exception {
restTemplate,
tokenEndpointBuilder,
new KeyInfoService("http://uaa.example.com"),
null
null,
false
) {
@Override
protected <T extends AbstractExternalOAuthIdentityProviderDefinition<T>> String getTokenFromCode(
Expand Down
Loading
Loading