Procházet zdrojové kódy

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
Giovanni Lovato před 11 měsíci
rodič
revize
a3fd551fb5

+ 40 - 0
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 <a href=
+	 * "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
+	 * Provider Configuration Response</a> to initialize the
+	 * {@link ClientRegistration.Builder}.
+	 *
+	 * <p>
+	 * 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).
+	 * </p>
+	 *
+	 * <p>
+	 * Example usage:
+	 * </p>
+	 * <pre>
+	 * RequestEntity&lt;Void&gt; request = RequestEntity.get(metadataEndpoint).build();
+	 * ParameterizedTypeReference&lt;Map&lt;String, Object&gt;&gt; typeReference = new ParameterizedTypeReference&lt;&gt;() {};
+	 * Map&lt;String, Object&gt; 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();
+	 * </pre>
+	 * @param the OpenID Provider configuration map
+	 * @return the {@link ClientRegistration} built from the configuration
+	 */
+	public static ClientRegistration.Builder fromOidcConfiguration(Map<String, Object> 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 <a href=
 	 * "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>

+ 114 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java

@@ -455,6 +455,120 @@ public class ClientRegistrationsTests {
 		// @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<String, Object> 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);