From f1eb74464b36028841d2c3e1e9aff5fe20f7452d Mon Sep 17 00:00:00 2001 From: sahariardev Date: Fri, 2 Sep 2022 19:56:11 +0600 Subject: [PATCH] Return registration_endpoint in OidcProviderConfigurationEndpointFilter Before: client registration endpoint was not retuned in oidc Provider Configuration response After: Returns client registration endpoint in oidcprovider configuration response if client registration is enabled Fixes gh-370 --- ...ractOAuth2AuthorizationServerMetadata.java | 11 ++ ...horizationServerMetadataClaimAccessor.java | 10 ++ ...AuthorizationServerMetadataClaimNames.java | 6 + .../web/configurers/OidcConfigurer.java | 23 +++ ...Auth2AuthorizationServerMetadataTests.java | 6 + ...idcProviderConfigurationMetaDataTests.java | 146 ++++++++++++++++++ 6 files changed, 202 insertions(+) create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationMetaDataTests.java diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java index bef3f0d55..dad438aca 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java @@ -274,6 +274,17 @@ public B tokenIntrospectionEndpointAuthenticationMethods(Consumer> 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. diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java index 35ced7177..d0873fbaa 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java @@ -141,6 +141,16 @@ default List getTokenIntrospectionEndpointAuthenticationMethods() { 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)}. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java index 46fdb3e74..6d33c7cde 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java +++ b/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 */ diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java index 9d8b9058b..e610fb963 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java @@ -23,8 +23,12 @@ 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 @@ void init(HttpSecurity httpSecurity) { @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)); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java index 49db4b50e..bf91cb974 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java @@ -60,6 +60,7 @@ public void buildWhenAllClaimsProvidedThenCreated() { .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 void buildWhenAllClaimsProvidedThenCreated() { 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 void withClaimsWhenClaimsProvidedThenCreated() { 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 void withClaimsWhenClaimsProvidedThenCreated() { 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 void withClaimsWhenClaimsWithUrlsProvidedThenCreated() { 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 void withClaimsWhenClaimsWithUrlsProvidedThenCreated() { 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"); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationMetaDataTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationMetaDataTests.java new file mode 100644 index 000000000..37dfdc9e3 --- /dev/null +++ b/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(); + } + + } + +}