ソースを参照

Return registration_endpoint when client registration is enabled

Closes gh-370
sahariardev 3 年 前
コミット
cd6f1d7dc3

+ 11 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java

@@ -274,6 +274,17 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
 			return getThis();
 		}
 
+		/**
+		 * Use this {@code registration_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, Optional.
+		 *
+		 * @param clientRegistrationEndpoint the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 0.4.0
+		 */
+		public B clientRegistrationEndpoint(String clientRegistrationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, clientRegistrationEndpoint);
+		}
+
 		/**
 		 * Add this Proof Key for Code Exchange (PKCE) {@code code_challenge_method} to the collection of {@code code_challenge_methods_supported}
 		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.

+ 10 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java

@@ -141,6 +141,16 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc
 		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED);
 	}
 
+	/**
+	 * Returns the {@code URL} of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint {@code (registration_endpoint)}.
+	 *
+	 * @return the {@code URL} of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint
+	 * @since 0.4.0
+	 */
+	default URL getClientRegistrationEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT);
+	}
+
 	/**
 	 * Returns the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported {@code (code_challenge_methods_supported)}.
 	 *

+ 6 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java

@@ -86,6 +86,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames {
 	 */
 	public static final String INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED = "introspection_endpoint_auth_methods_supported";
 
+	/**
+	 * {@code registration_endpoint} -  the {@code URL} of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint
+	 * @since 0.4.0
+	 */
+	public static final String REGISTRATION_ENDPOINT = "registration_endpoint";
+
 	/**
 	 * {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported
 	 */

+ 23 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java

@@ -23,8 +23,12 @@ import java.util.Map;
 import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.web.util.UriComponentsBuilder;
 
 /**
  * Configurer for OpenID Connect 1.0 support.
@@ -102,6 +106,25 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
 
 	@Override
 	void configure(HttpSecurity httpSecurity) {
+		OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
+				getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
+		if (clientRegistrationEndpointConfigurer != null) {
+			OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer =
+					getConfigurer(OidcProviderConfigurationEndpointConfigurer.class);
+
+			providerConfigurationEndpointConfigurer
+					.addDefaultProviderConfigurationCustomizer(builder -> {
+						AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
+						String issuer = authorizationServerContext.getIssuer();
+						AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
+
+						String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
+								.path(authorizationServerSettings.getOidcClientRegistrationEndpoint()).build().toUriString();
+
+						builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
+					});
+		}
+
 		this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity));
 	}
 

+ 6 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java

@@ -60,6 +60,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 				.tokenRevocationEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
 				.tokenIntrospectionEndpoint("https://example.com/issuer1/oauth2/introspect")
 				.tokenIntrospectionEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
+				.clientRegistrationEndpoint("https://example.com/issuer1/connect/register")
 				.codeChallengeMethod("S256")
 				.claim("a-claim", "a-value")
 				.build();
@@ -76,6 +77,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
+		assertThat(authorizationServerMetadata.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
 		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactly("S256");
 		assertThat(authorizationServerMetadata.getClaimAsString("a-claim")).isEqualTo("a-value");
 	}
@@ -115,6 +117,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 		claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
 		claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, "https://example.com/issuer1/oauth2/revoke");
 		claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, "https://example.com/issuer1/oauth2/introspect");
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, "https://example.com/issuer1/connect/register");
 		claims.put("some-claim", "some-value");
 
 		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.withClaims(claims).build();
@@ -131,6 +134,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull();
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
 		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull();
 		assertThat(authorizationServerMetadata.getClaimAsString("some-claim")).isEqualTo("some-value");
 	}
@@ -145,6 +149,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 		claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
 		claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, url("https://example.com/issuer1/oauth2/revoke"));
 		claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, url("https://example.com/issuer1/oauth2/introspect"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, url("https://example.com/issuer1/connect/register"));
 		claims.put("some-claim", "some-value");
 
 		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.withClaims(claims).build();
@@ -161,6 +166,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull();
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
 		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
 		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull();
 		assertThat(authorizationServerMetadata.getClaimAsString("some-claim")).isEqualTo("some-value");
 	}

+ 146 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationMetaDataTests.java

@@ -0,0 +1,146 @@
+package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultMatcher;
+
+import static org.springframework.test.web.servlet.ResultMatcher.matchAll;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Integration tests for OpenID Provider Configuration Endpoint.
+ *
+ * @author Sahariar Alam Khandoker
+ */
+public class OidcProviderConfigurationMetaDataTests {
+	private static final String DEFAULT_OAUTH2_PROVIDER_CONFIGURATION_METADATA_ENDPOINT_URI = "/.well-known/openid-configuration";
+	private static final String issuerUrl = "https://example.com/issuer1";
+
+	@Rule
+	public final SpringTestRule spring = new SpringTestRule();
+
+	@Autowired
+	private MockMvc mvc;
+
+	@Test
+	public void requestWhenProviderConfigurationRequestGetTheProviderConfigurationResponseWithoutRegistrationEndpoint() throws Exception {
+		this.spring.register(AuthorizationServerConfiguration.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OAUTH2_PROVIDER_CONFIGURATION_METADATA_ENDPOINT_URI))
+				.andExpect(status().is2xxSuccessful())
+				.andExpect(providerConfigurationResponse())
+				.andExpect(jsonPath("$.registration_endpoint").doesNotExist())
+				.andReturn();
+	}
+
+	@Test
+	public void requestWhenProviderConfigurationWithClientRegistrationEnabledRequestGetTheProviderConfigurationResponseWithRegistrationEndpoint() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OAUTH2_PROVIDER_CONFIGURATION_METADATA_ENDPOINT_URI))
+				.andExpect(status().is2xxSuccessful())
+				.andExpect(providerConfigurationResponse())
+				.andExpect(jsonPath("$.registration_endpoint").value("https://example.com/issuer1/connect/register"))
+				.andReturn();
+	}
+
+	private static ResultMatcher providerConfigurationResponse() {
+		// @formatter:off
+		return matchAll(
+				jsonPath("issuer").value("https://example.com/issuer1"),
+				jsonPath("authorization_endpoint").value("https://example.com/issuer1/oauth2/authorize"),
+				jsonPath("token_endpoint").value("https://example.com/issuer1/oauth2/token"),
+				jsonPath("jwks_uri").value("https://example.com/issuer1/oauth2/jwks"),
+				jsonPath("scopes_supported").value("openid"),
+				jsonPath("response_types_supported").value("code"),
+				jsonPath("$.grant_types_supported[0]").value("authorization_code"),
+				jsonPath("$.grant_types_supported[1]").value("client_credentials"),
+				jsonPath("$.grant_types_supported[2]").value("refresh_token"),
+				jsonPath("revocation_endpoint").value("https://example.com/issuer1/oauth2/revoke"),
+				jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value("client_secret_basic"),
+				jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value("client_secret_post"),
+				jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value("client_secret_jwt"),
+				jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value("private_key_jwt"),
+				jsonPath("introspection_endpoint").value("https://example.com/issuer1/oauth2/introspect"),
+				jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value("client_secret_basic"),
+				jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value("client_secret_post"),
+				jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value("client_secret_jwt"),
+				jsonPath("$.introspection_endpoint_auth_methods_supported[3]").value("private_key_jwt"),
+				jsonPath("subject_types_supported").value("public"),
+				jsonPath("id_token_signing_alg_values_supported").value("RS256"),
+				jsonPath("userinfo_endpoint").value("https://example.com/issuer1/userinfo"),
+				jsonPath("$.token_endpoint_auth_methods_supported[0]").value("client_secret_basic"),
+				jsonPath("$.token_endpoint_auth_methods_supported[1]").value("client_secret_post"),
+				jsonPath("$.token_endpoint_auth_methods_supported[2]").value("client_secret_jwt"),
+				jsonPath("$.token_endpoint_auth_methods_supported[3]").value("private_key_jwt")
+		);
+		// @formatter:on
+	}
+
+
+	@EnableWebSecurity
+	static class AuthorizationServerConfigurationWithClientRegistrationEnabled extends AuthorizationServerConfiguration {
+		@Bean
+		SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+					new OAuth2AuthorizationServerConfigurer();
+			http.apply(authorizationServerConfigurer);
+
+			authorizationServerConfigurer
+					.oidc(oidc ->
+							oidc
+									.clientRegistrationEndpoint(Customizer.withDefaults())
+					);
+
+			return http.build();
+		}
+	}
+
+	@EnableWebSecurity
+	static class AuthorizationServerConfiguration {
+
+		@Bean
+		SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
+			// @formatter:off
+			http
+					.exceptionHandling(exceptions ->
+							exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
+					);
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		RegisteredClientRepository registeredClientRepository() {
+			RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+			return new InMemoryRegisteredClientRepository(registeredClient);
+		}
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder()
+					.issuer(issuerUrl)
+					.build();
+		}
+
+	}
+
+}