Add issuerUri to ClientRegistration.providerDetails

- Add "issuerUri" attribute to ClientRegistration.providerDetails for OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization Server Metadata.
- Validate OidcIdToken "iss" claim against the OpenID Provider "issuerUri" value.
- Update documentation for client registration: it includes issuer-uri property now.

Fixes gh-8326
This commit is contained in:
Thomas Vitale 2020-05-07 22:53:08 +02:00 committed by Joe Grandja
parent db4ca1f756
commit 78fa859798
9 changed files with 40 additions and 9 deletions

View File

@ -42,6 +42,7 @@ public enum CommonOAuth2Provider {
builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
builder.issuerUri("https://accounts.google.com");
builder.userNameAttributeName(IdTokenClaimNames.SUB);
builder.clientName("Google");
return builder;

View File

@ -165,6 +165,7 @@ public class ClientRegistrationsBeanDefinitionParserTests {
.isEqualTo(AuthenticationMethod.HEADER);
assertThat(googleProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("sub");
assertThat(googleProviderDetails.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs");
assertThat(googleProviderDetails.getIssuerUri()).isEqualTo(serverUrl);
}
@Test
@ -195,6 +196,7 @@ public class ClientRegistrationsBeanDefinitionParserTests {
.isEqualTo(AuthenticationMethod.HEADER);
assertThat(googleProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("sub");
assertThat(googleProviderDetails.getJwkSetUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/certs");
assertThat(googleProviderDetails.getIssuerUri()).isEqualTo("https://accounts.google.com");
ClientRegistration githubRegistration = clientRegistrationRepository.findByRegistrationId("github-login");
assertThat(githubRegistration).isNotNull();

View File

@ -137,9 +137,11 @@ The following table outlines the mapping of the Spring Boot 2.x OAuth Client pro
|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method`
|`providerDetails.userInfoEndpoint.authenticationMethod`
|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute`
|`providerDetails.userInfoEndpoint.userNameAttributeName`
|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri`
|`providerDetails.issuerUri`
|===
[TIP]

View File

@ -69,8 +69,7 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator<Jwt> {
// 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
// MUST exactly match the value of the iss (issuer) Claim.
String metadataIssuer = (String) this.clientRegistration.getProviderDetails().getConfigurationMetadata()
.get("issuer");
String metadataIssuer = this.clientRegistration.getProviderDetails().getIssuerUri();
if (metadataIssuer != null && !Objects.equals(metadataIssuer, idToken.getIssuer().toExternalForm())) {
invalidClaims.put(IdTokenClaimNames.ISS, idToken.getIssuer());

View File

@ -163,6 +163,7 @@ public final class ClientRegistration implements Serializable {
private String tokenUri;
private UserInfoEndpoint userInfoEndpoint = new UserInfoEndpoint();
private String jwkSetUri;
private String issuerUri;
private Map<String, Object> configurationMetadata = Collections.emptyMap();
private ProviderDetails() {
@ -204,6 +205,16 @@ public final class ClientRegistration implements Serializable {
return this.jwkSetUri;
}
/**
* Returns the uri for the OpenID Provider Issuer.
*
* @since 5.4
* @return the uri for the OpenID Provider Issuer
*/
public String getIssuerUri() {
return this.issuerUri;
}
/**
* Returns a {@code Map} of the metadata describing the provider's configuration.
*
@ -296,6 +307,7 @@ public final class ClientRegistration implements Serializable {
private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER;
private String userNameAttributeName;
private String jwkSetUri;
private String issuerUri;
private Map<String, Object> configurationMetadata = Collections.emptyMap();
private String clientName;
@ -317,6 +329,7 @@ public final class ClientRegistration implements Serializable {
this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod;
this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName;
this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri;
this.issuerUri = clientRegistration.providerDetails.issuerUri;
Map<String, Object> configurationMetadata = clientRegistration.providerDetails.configurationMetadata;
if (configurationMetadata != EMPTY_MAP) {
this.configurationMetadata = new HashMap<>(configurationMetadata);
@ -486,6 +499,17 @@ public final class ClientRegistration implements Serializable {
return this;
}
/**
* Sets the uri for the OpenID Provider Issuer.
*
* @param issuerUri the uri for the OpenID Provider Issuer
* @return the {@link Builder}
*/
public Builder issuerUri(String issuerUri) {
this.issuerUri = issuerUri;
return this;
}
/**
* Sets the metadata describing the provider's configuration.
*
@ -554,6 +578,7 @@ public final class ClientRegistration implements Serializable {
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;
providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName;
providerDetails.jwkSetUri = this.jwkSetUri;
providerDetails.issuerUri = this.issuerUri;
providerDetails.configurationMetadata = Collections.unmodifiableMap(this.configurationMetadata);
clientRegistration.providerDetails = providerDetails;

View File

@ -248,6 +248,7 @@ public final class ClientRegistrations {
.authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
.providerConfigurationMetadata(configurationMetadata)
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
.issuerUri(issuer)
.clientName(issuer);
}

View File

@ -139,6 +139,8 @@ public class OAuth2AuthorizedClientMixinTests {
.isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
assertThat(clientRegistration.getProviderDetails().getJwkSetUri())
.isEqualTo(expectedClientRegistration.getProviderDetails().getJwkSetUri());
assertThat(clientRegistration.getProviderDetails().getIssuerUri())
.isEqualTo(expectedClientRegistration.getProviderDetails().getIssuerUri());
assertThat(clientRegistration.getProviderDetails().getConfigurationMetadata())
.containsExactlyEntriesOf(clientRegistration.getProviderDetails().getConfigurationMetadata());
assertThat(clientRegistration.getClientName())
@ -203,6 +205,7 @@ public class OAuth2AuthorizedClientMixinTests {
.isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod());
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).isNull();
assertThat(clientRegistration.getProviderDetails().getJwkSetUri()).isNull();
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNull();
assertThat(clientRegistration.getProviderDetails().getConfigurationMetadata()).isEmpty();
assertThat(clientRegistration.getClientName())
.isEqualTo(clientRegistration.getRegistrationId());
@ -276,6 +279,7 @@ public class OAuth2AuthorizedClientMixinTests {
" \"userNameAttributeName\": " + (userInfoEndpoint.getUserNameAttributeName() != null ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + "\n" +
" },\n" +
" \"jwkSetUri\": " + (providerDetails.getJwkSetUri() != null ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" +
" \"issuerUri\": " + (providerDetails.getIssuerUri() != null ? "\"" + providerDetails.getIssuerUri() + "\"" : null) + ",\n" +
" \"configurationMetadata\": {\n" +
" " + configurationMetadata + "\n" +
" }\n" +

View File

@ -98,9 +98,7 @@ public class OidcIdTokenValidatorTests {
* When the issuer is set in the provider metadata, and it does not match the issuer in the ID Token,
* the validation must fail
*/
Map<String, Object> configurationMetadata = new HashMap<>();
configurationMetadata.put("issuer", "https://issuer.somethingelse.com");
this.registration = this.registration.providerConfigurationMetadata(configurationMetadata);
this.registration = this.registration.issuerUri("https://issuer.somethingelse.com");
assertThat(this.validateIdToken())
.hasSize(1)
@ -114,9 +112,7 @@ public class OidcIdTokenValidatorTests {
* When the issuer is set in the provider metadata, and it does match the issuer in the ID Token,
* the validation must succeed
*/
Map<String, Object> configurationMetadata = new HashMap<>();
configurationMetadata.put("issuer", "https://issuer.example.com");
this.registration = this.registration.providerConfigurationMetadata(configurationMetadata);
this.registration = this.registration.issuerUri("https://issuer.example.com");
assertThat(this.validateIdToken()).isEmpty();
}

View File

@ -162,6 +162,7 @@ public class ClientRegistrationsTest {
assertThat(provider.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth");
assertThat(provider.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token");
assertThat(provider.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs");
assertThat(provider.getIssuerUri()).isEqualTo(this.issuer);
assertThat(provider.getConfigurationMetadata()).containsKeys("authorization_endpoint", "claims_supported",
"code_challenge_methods_supported", "id_token_signing_alg_values_supported", "issuer", "jwks_uri",
"response_types_supported", "revocation_endpoint", "scopes_supported", "subject_types_supported",