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);