From 9dbec0450e373a529bd9ad7e297208776327d661 Mon Sep 17 00:00:00 2001 From: Giovanni Lovato Date: Fri, 20 Sep 2024 08:50:37 +0200 Subject: [PATCH] Add ClientRegistrations.fromOidcConfiguration method ClientRegistrations now provides the fromOidcConfiguration method to create a ClientRegistration.Builder from a map representation of an OpenID Provider Configuration Response. This is useful when the OpenID Provider Configuration is not available at a well-known location, or if custom validation is needed for the issuer location (e.g. if the issuer is only reachable via a back-channel URI that is different from the issuer value in the configuration). Fixes: gh-14633 --- .../registration/ClientRegistrations.java | 40 ++++++ .../ClientRegistrationsTests.java | 114 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java index 7fb2b889f73..a003cd22ef8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java @@ -72,6 +72,46 @@ public final class ClientRegistrations { private ClientRegistrations() { } + /** + * Creates a {@link ClientRegistration.Builder} using the provided map representation + * of an OpenID + * Provider Configuration Response to initialize the + * {@link ClientRegistration.Builder}. + * + *

+ * This is useful when the OpenID Provider Configuration is not available at a + * well-known location, or if custom validation is needed for the issuer location + * (e.g. if the issuer is only accessible from a back-channel URI that is different + * from the issuer value in the configuration). + *

+ * + *

+ * Example usage: + *

+ *
+	 * RequestEntity<Void> request = RequestEntity.get(metadataEndpoint).build();
+	 * ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<>() {};
+	 * Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
+	 * // Validate configuration.get("issuer") as per in the OIDC specification
+	 * ClientRegistration registration = ClientRegistrations.fromOidcConfiguration(configuration)
+	 *     .clientId("client-id")
+	 *     .clientSecret("client-secret")
+	 *     .build();
+	 * 
+ * @param the OpenID Provider configuration map + * @return the {@link ClientRegistration} built from the configuration + */ + public static ClientRegistration.Builder fromOidcConfiguration(Map configuration) { + OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse); + ClientRegistration.Builder builder = withProviderConfiguration(metadata, metadata.getIssuer().getValue()); + builder.jwkSetUri(metadata.getJWKSetURI().toASCIIString()); + if (metadata.getUserInfoEndpointURI() != null) { + builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString()); + } + return builder; + } + /** * Creates a {@link ClientRegistration.Builder} using the provided Issuer diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java index b3b74e805d6..59c0fb05288 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java @@ -455,6 +455,120 @@ public void issuerWhenOAuth2ConfigurationDoesNotMatchThenMeaningfulErrorMessage( // @formatter:on } + @Test + public void issuerWhenOidcConfigurationAllInformationThenSuccess() throws Exception { + ClientRegistration registration = registration(this.response).build(); + ClientRegistration.ProviderDetails provider = registration.getProviderDetails(); + assertIssuerMetadata(registration, provider); + assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo"); + } + + private ClientRegistration.Builder registration(Map configuration) { + this.issuer = "https://example.com"; + return ClientRegistrations.fromOidcConfiguration(configuration) + .clientId("client-id") + .clientSecret("client-secret"); + } + + @Test + public void issuerWhenOidcConfigurationResponseMissingJwksUriThenThrowsIllegalArgumentException() throws Exception { + this.response.remove("jwks_uri"); + assertThatIllegalArgumentException().isThrownBy(() -> registration(this.response).build()) + .withMessageContaining("The public JWK set URI must not be null"); + } + + @Test + public void issuerWhenOidcConfigurationResponseMissingUserInfoUriThenSuccess() throws Exception { + this.response.remove("userinfo_endpoint"); + ClientRegistration registration = registration(this.response).build(); + assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUri()).isNull(); + } + + @Test + public void issuerWhenOidcConfigurationGrantTypesSupportedNullThenDefaulted() throws Exception { + this.response.remove("grant_types_supported"); + ClientRegistration registration = registration(this.response).build(); + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + } + + @Test + public void issuerWhenOidcConfigurationImplicitGrantTypeThenSuccess() throws Exception { + this.response.put("grant_types_supported", Arrays.asList("implicit")); + ClientRegistration registration = registration(this.response).build(); + // The authorization_code grant type is still the default + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + } + + @Test + public void issuerWhenOidcConfigurationResponseAuthorizationEndpointIsNullThenSuccess() throws Exception { + this.response.put("grant_types_supported", Arrays.asList("urn:ietf:params:oauth:grant-type:jwt-bearer")); + this.response.remove("authorization_endpoint"); + ClientRegistration registration = registration(this.response) + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .build(); + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER); + ClientRegistration.ProviderDetails provider = registration.getProviderDetails(); + assertThat(provider.getAuthorizationUri()).isNull(); + } + + @Test + public void issuerWhenOidcConfigurationTokenEndpointAuthMethodsNullThenDefaulted() throws Exception { + this.response.remove("token_endpoint_auth_methods_supported"); + ClientRegistration registration = registration(this.response).build(); + assertThat(registration.getClientAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + } + + @Test + public void issuerWhenOidcConfigurationClientSecretBasicAuthMethodThenMethodIsBasic() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_basic")); + ClientRegistration registration = registration(this.response).build(); + assertThat(registration.getClientAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + } + + @Test + public void issuerWhenOidcConfigurationTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); + ClientRegistration registration = registration(this.response).build(); + assertThat(registration.getClientAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_POST); + } + + @Test + public void issuerWhenOidcConfigurationClientSecretJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_jwt")); + ClientRegistration registration = registration(this.response).build(); + // The client_secret_basic auth method is still the default + assertThat(registration.getClientAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + } + + @Test + public void issuerWhenOidcConfigurationPrivateKeyJwtAuthMethodThenMethodIsClientSecretBasic() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("private_key_jwt")); + ClientRegistration registration = registration(this.response).build(); + // The client_secret_basic auth method is still the default + assertThat(registration.getClientAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + } + + @Test + public void issuerWhenOidcConfigurationTokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none")); + ClientRegistration registration = registration(this.response).build(); + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE); + } + + @Test + public void issuerWhenOidcConfigurationTlsClientAuthMethodThenSuccess() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth")); + ClientRegistration registration = registration(this.response).build(); + // The client_secret_basic auth method is still the default + assertThat(registration.getClientAuthenticationMethod()) + .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + } + private ClientRegistration.Builder registration(String path) throws Exception { this.issuer = createIssuerFromServer(path); this.response.put("issuer", this.issuer);