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 dad438ac..b7c7b81f 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 @@ -275,7 +275,7 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth } /** - * Use this {@code registration_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, Optional. + * 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 @@ -380,6 +380,9 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods must be of type List"); Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods cannot be empty"); } + if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT) != null) { + validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT), "clientRegistrationEndpoint must be a valid URL"); + } if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED) != null) { Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods must be of type List"); Assert.notEmpty((List) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods cannot be empty"); 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 d0873fba..8767843f 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 @@ -142,9 +142,9 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc } /** - * Returns the {@code URL} of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint {@code (registration_endpoint)}. + * Returns the {@code URL} of the 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 + * @return the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint * @since 0.4.0 */ default URL getClientRegistrationEndpoint() { 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 6d33c7cd..a4fb116a 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 @@ -87,7 +87,7 @@ 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 + * {@code registration_endpoint} - the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint * @since 0.4.0 */ public static final String REGISTRATION_ENDPOINT = "registration_endpoint"; 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 e610fb96..5ce9cbc3 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 @@ -113,7 +113,7 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer { getConfigurer(OidcProviderConfigurationEndpointConfigurer.class); providerConfigurationEndpointConfigurer - .addDefaultProviderConfigurationCustomizer(builder -> { + .addDefaultProviderConfigurationCustomizer((builder) -> { AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext(); String issuer = authorizationServerContext.getIssuer(); AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings(); 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 bf91cb97..49db4b50 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,7 +60,6 @@ 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(); @@ -77,7 +76,6 @@ 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"); } @@ -117,7 +115,6 @@ 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(); @@ -134,7 +131,6 @@ 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"); } @@ -149,7 +145,6 @@ 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(); @@ -166,7 +161,6 @@ 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"); } 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 deleted file mode 100644 index 37dfdc9e..00000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationMetaDataTests.java +++ /dev/null @@ -1,146 +0,0 @@ -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(); - } - - } - -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java new file mode 100644 index 00000000..30f25fcc --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +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.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +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.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +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 the OpenID Connect 1.0 Provider Configuration endpoint. + * + * @author Sahariar Alam Khandoker + * @author Joe Grandja + */ +public class OidcProviderConfigurationTests { + private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration"; + private static final String ISSUER_URL = "https://example.com/issuer1"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private AuthorizationServerSettings authorizationServerSettings; + + @Autowired + private MockMvc mvc; + + @Test + public void requestWhenConfigurationRequestThenDefaultConfigurationResponse() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers()) + .andExpect(jsonPath("$.registration_endpoint").doesNotExist()); + } + + @Test + public void requestWhenConfigurationRequestAndClientRegistrationEnabledThenConfigurationResponseIncludesRegistrationEndpoint() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire(); + + this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers()) + .andExpect(jsonPath("$.registration_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint()))); + } + + private ResultMatcher[] defaultConfigurationMatchers() { + // @formatter:off + return new ResultMatcher[] { + jsonPath("issuer").value(ISSUER_URL), + jsonPath("authorization_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getAuthorizationEndpoint())), + jsonPath("token_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenEndpoint())), + jsonPath("$.token_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), + jsonPath("$.token_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), + jsonPath("$.token_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), + jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), + jsonPath("jwks_uri").value(ISSUER_URL.concat(this.authorizationServerSettings.getJwkSetEndpoint())), + jsonPath("userinfo_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())), + jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()), + jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), + jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()), + jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()), + jsonPath("revocation_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())), + jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), + jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), + jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), + jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), + jsonPath("introspection_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint())), + jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), + jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), + jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), + jsonPath("$.introspection_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), + jsonPath("subject_types_supported").value("public"), + jsonPath("id_token_signing_alg_values_supported").value(SignatureAlgorithm.RS256.getName()), + jsonPath("scopes_supported").value(OidcScopes.OPENID) + }; + // @formatter:on + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfiguration { + + @Bean + RegisteredClientRepository registeredClientRepository() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + return new InMemoryRegisteredClientRepository(registeredClient); + } + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder() + .issuer(ISSUER_URL) + .build(); + } + + } + + @Configuration + @EnableWebSecurity + static class AuthorizationServerConfigurationWithClientRegistrationEnabled extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + http.apply(authorizationServerConfigurer); + + authorizationServerConfigurer + .oidc(oidc -> + oidc.clientRegistrationEndpoint(Customizer.withDefaults()) + ); + + return http.build(); + } + // @formatter:on + + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java index dff34428..ce6d83ff 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java @@ -61,6 +61,7 @@ public class OidcProviderConfigurationTests { .idTokenSigningAlgorithm("RS256") .userInfoEndpoint("https://example.com/issuer1/userinfo") .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) + .clientRegistrationEndpoint("https://example.com/issuer1/connect/register") .claim("a-claim", "a-value") .build(); @@ -75,6 +76,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo")); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register")); assertThat(providerConfiguration.getClaim("a-claim")).isEqualTo("a-value"); } @@ -115,6 +117,7 @@ public class OidcProviderConfigurationTests { claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public")); claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256")); claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, "https://example.com/issuer1/userinfo"); + claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, "https://example.com/issuer1/connect/register"); claims.put("some-claim", "some-value"); OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build(); @@ -130,6 +133,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo")); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register")); assertThat(providerConfiguration.getClaim("some-claim")).isEqualTo("some-value"); } @@ -145,6 +149,7 @@ public class OidcProviderConfigurationTests { claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public")); claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256")); claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, url("https://example.com/issuer1/userinfo")); + claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, url("https://example.com/issuer1/connect/register")); claims.put("some-claim", "some-value"); OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build(); @@ -160,6 +165,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo")); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register")); assertThat(providerConfiguration.getClaim("some-claim")).isEqualTo("some-value"); } @@ -396,6 +402,16 @@ public class OidcProviderConfigurationTests { .withMessage("userInfoEndpoint must be a valid URL"); } + @Test + public void buildWhenClientRegistrationEndpointNotUrlThenThrowIllegalArgumentException() { + OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder + .claims((claims) -> claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, "not an url")); + + assertThatIllegalArgumentException() + .isThrownBy(builder::build) + .withMessage("clientRegistrationEndpoint must be a valid URL"); + } + @Test public void responseTypesWhenAddingOrRemovingThenCorrectValues() { OidcProviderConfiguration configuration = this.minimalConfigurationBuilder