From 865fcf9a792e42598db54359c1176ab222190e4a Mon Sep 17 00:00:00 2001 From: addcontent Date: Sat, 4 Apr 2026 11:56:05 +0300 Subject: [PATCH] Add allowed scopes validation for DCR endpoints Add allowedScopes configuration to both OAuth2 and OIDC Dynamic Client Registration endpoints. When configured, registration requests containing scopes not in the allowed set are rejected with an invalid_scope error. This addresses the scenario where DCR accepts arbitrary scope strings verbatim, which could result in dynamically registered clients obtaining tokens with unintended privilege scopes. Changes: - Add setAllowedScopes(Set) to both authentication providers - Add allowedScopes(String...) to both endpoint configurers - Add documentation with security considerations for DCR scope validation - Add tests for scope validation, backward compatibility, and null safety Signed-off-by: addcontent --- ...2ClientRegistrationEndpointConfigurer.java | 27 +++- ...cClientRegistrationEndpointConfigurer.java | 29 ++++- .../protocol-endpoints.adoc | 61 +++++++++ ...entRegistrationAuthenticationProvider.java | 22 ++++ ...entRegistrationAuthenticationProvider.java | 22 ++++ ...gistrationAuthenticationProviderTests.java | 116 +++++++++++++++++ ...gistrationAuthenticationProviderTests.java | 118 ++++++++++++++++++ 7 files changed, 391 insertions(+), 4 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientRegistrationEndpointConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientRegistrationEndpointConfigurer.java index c6b5931f539..73b2e83e89d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientRegistrationEndpointConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientRegistrationEndpointConfigurer.java @@ -17,7 +17,10 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import jakarta.servlet.http.HttpServletRequest; @@ -74,6 +77,8 @@ public final class OAuth2ClientRegistrationEndpointConfigurer extends AbstractOA private boolean openRegistrationAllowed; + private Set allowedScopes; + /** * Restrict for internal use only. * @param objectPostProcessor an {@code ObjectPostProcessor} @@ -195,6 +200,21 @@ public OAuth2ClientRegistrationEndpointConfigurer openRegistrationAllowed(boolea return this; } + /** + * Sets the allowed scopes for client registration. When set, only the specified + * scopes will be accepted during Dynamic Client Registration. If not set, any scope + * value will be accepted. + * @param allowedScopes the allowed scopes + * @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further + * configuration + * @since 7.1 + */ + public OAuth2ClientRegistrationEndpointConfigurer allowedScopes(String... allowedScopes) { + Assert.notEmpty(allowedScopes, "allowedScopes cannot be empty"); + this.allowedScopes = new HashSet<>(Arrays.asList(allowedScopes)); + return this; + } + @Override void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils @@ -207,7 +227,7 @@ void init(HttpSecurity httpSecurity) { .matcher(HttpMethod.POST, clientRegistrationEndpointUri); List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity, - this.openRegistrationAllowed); + this.openRegistrationAllowed, this.allowedScopes); if (!this.authenticationProviders.isEmpty()) { authenticationProviders.addAll(0, this.authenticationProviders); } @@ -258,7 +278,7 @@ private static List createDefaultAuthenticationConverte } private static List createDefaultAuthenticationProviders(HttpSecurity httpSecurity, - boolean openRegistrationAllowed) { + boolean openRegistrationAllowed, Set allowedScopes) { List authenticationProviders = new ArrayList<>(); OAuth2ClientRegistrationAuthenticationProvider clientRegistrationAuthenticationProvider = new OAuth2ClientRegistrationAuthenticationProvider( @@ -269,6 +289,9 @@ private static List createDefaultAuthenticationProviders clientRegistrationAuthenticationProvider.setPasswordEncoder(passwordEncoder); } clientRegistrationAuthenticationProvider.setOpenRegistrationAllowed(openRegistrationAllowed); + if (allowedScopes != null) { + clientRegistrationAuthenticationProvider.setAllowedScopes(allowedScopes); + } authenticationProviders.add(clientRegistrationAuthenticationProvider); return authenticationProviders; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationEndpointConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationEndpointConfigurer.java index a83fb98233f..d6111d1e9dd 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationEndpointConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationEndpointConfigurer.java @@ -17,7 +17,10 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import jakarta.servlet.http.HttpServletRequest; @@ -75,6 +78,8 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut private AuthenticationFailureHandler errorResponseHandler; + private Set allowedScopes; + /** * Restrict for internal use only. * @param objectPostProcessor an {@code ObjectPostProcessor} @@ -182,6 +187,21 @@ public OidcClientRegistrationEndpointConfigurer errorResponseHandler( return this; } + /** + * Sets the allowed scopes for client registration. When set, only the specified + * scopes will be accepted during Dynamic Client Registration. If not set, any scope + * value will be accepted. + * @param allowedScopes the allowed scopes + * @return the {@link OidcClientRegistrationEndpointConfigurer} for further + * configuration + * @since 7.1 + */ + public OidcClientRegistrationEndpointConfigurer allowedScopes(String... allowedScopes) { + Assert.notEmpty(allowedScopes, "allowedScopes cannot be empty"); + this.allowedScopes = new HashSet<>(Arrays.asList(allowedScopes)); + return this; + } + @Override void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils @@ -194,7 +214,8 @@ void init(HttpSecurity httpSecurity) { PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, clientRegistrationEndpointUri), PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, clientRegistrationEndpointUri)); - List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); + List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity, + this.allowedScopes); if (!this.authenticationProviders.isEmpty()) { authenticationProviders.addAll(0, this.authenticationProviders); } @@ -245,7 +266,8 @@ private static List createDefaultAuthenticationConverte return authenticationConverters; } - private static List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { + private static List createDefaultAuthenticationProviders(HttpSecurity httpSecurity, + Set allowedScopes) { List authenticationProviders = new ArrayList<>(); OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider = new OidcClientRegistrationAuthenticationProvider( @@ -256,6 +278,9 @@ private static List createDefaultAuthenticationProviders if (passwordEncoder != null) { oidcClientRegistrationAuthenticationProvider.setPasswordEncoder(passwordEncoder); } + if (allowedScopes != null) { + oidcClientRegistrationAuthenticationProvider.setAllowedScopes(allowedScopes); + } authenticationProviders.add(oidcClientRegistrationAuthenticationProvider); OidcClientConfigurationAuthenticationProvider oidcClientConfigurationAuthenticationProvider = new OidcClientConfigurationAuthenticationProvider( diff --git a/docs/modules/ROOT/pages/servlet/oauth2/authorization-server/protocol-endpoints.adoc b/docs/modules/ROOT/pages/servlet/oauth2/authorization-server/protocol-endpoints.adoc index 967ff73adc7..a5ec5763d67 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/authorization-server/protocol-endpoints.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/authorization-server/protocol-endpoints.adoc @@ -730,6 +730,35 @@ The access token in a Client Registration request *REQUIRES* the OAuth2 scope `c [TIP] To allow open client registration (no access token in request), configure `OAuth2ClientRegistrationAuthenticationProvider.setOpenRegistrationAllowed(true)`. +[[oauth2AuthorizationServer-oauth2-client-registration-endpoint-scope-validation]] +=== Customizing Scope Validation + +By default, the OAuth2 Client Registration endpoint accepts any scope values in the registration request. +It is recommended to configure the allowed scopes to restrict which scopes can be assigned to dynamically registered clients. + +[source,java] +---- +@Bean +public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + http + .oauth2AuthorizationServer((authorizationServer) -> + authorizationServer + .clientRegistrationEndpoint(clientRegistrationEndpoint -> + clientRegistrationEndpoint + .allowedScopes("openid", "profile", "email") // <1> + ) + ); + + return http.build(); +} +---- +<1> `allowedScopes()`: Restricts which scope values are accepted during client registration. Registration requests containing scopes not in this set will be rejected with an `invalid_scope` error. + +[IMPORTANT] +When enabling Dynamic Client Registration, it is recommended to always configure `allowedScopes()` to prevent dynamically registered clients from requesting arbitrary scope values. + +For advanced scope validation logic, use `setRegisteredClientConverter()` to provide a custom `Converter` that performs the required validation. + [[oauth2AuthorizationServer-oauth2-authorization-server-metadata-endpoint]] == OAuth2 Authorization Server Metadata Endpoint @@ -1041,3 +1070,35 @@ The access token in a Client Registration request *REQUIRES* the OAuth2 scope `c [IMPORTANT] The access token in a Client Read request *REQUIRES* the OAuth2 scope `client.read`. + +[[oauth2AuthorizationServer-oidc-client-registration-endpoint-scope-validation]] +=== Customizing Scope Validation + +By default, the OIDC Client Registration endpoint accepts any scope values in the registration request. +It is recommended to configure the allowed scopes to restrict which scopes can be assigned to dynamically registered clients. + +[source,java] +---- +@Bean +public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + http + .oauth2AuthorizationServer((authorizationServer) -> + authorizationServer + .oidc(oidc -> + oidc + .clientRegistrationEndpoint(clientRegistrationEndpoint -> + clientRegistrationEndpoint + .allowedScopes("openid", "profile", "email") // <1> + ) + ) + ); + + return http.build(); +} +---- +<1> `allowedScopes()`: Restricts which scope values are accepted during client registration. Registration requests containing scopes not in this set will be rejected with an `invalid_scope` error. + +[IMPORTANT] +When enabling Dynamic Client Registration, it is recommended to always configure `allowedScopes()` to prevent dynamically registered clients from requesting arbitrary scope values. + +For advanced scope validation logic, use `setRegisteredClientConverter()` to provide a custom `Converter` that performs the required validation. diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationProvider.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationProvider.java index fb08300f841..b95a7c4e7e6 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationProvider.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationProvider.java @@ -87,6 +87,8 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut private boolean openRegistrationAllowed; + private Set allowedScopes; + /** * Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the * provided parameters. @@ -200,6 +202,18 @@ public void setOpenRegistrationAllowed(boolean openRegistrationAllowed) { this.openRegistrationAllowed = openRegistrationAllowed; } + /** + * Sets the allowed scopes for client registration. When set, only the specified + * scopes will be accepted during Dynamic Client Registration. If not set, any scope + * value will be accepted. + * @param allowedScopes the {@code Set} of allowed scopes + * @since 7.1 + */ + public void setAllowedScopes(Set allowedScopes) { + Assert.notNull(allowedScopes, "allowedScopes cannot be null"); + this.allowedScopes = allowedScopes; + } + private OAuth2ClientRegistrationAuthenticationToken registerClient( OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication, @Nullable OAuth2Authorization authorization) { @@ -210,6 +224,14 @@ private OAuth2ClientRegistrationAuthenticationToken registerClient( OAuth2ClientMetadataClaimNames.REDIRECT_URIS); } + if (this.allowedScopes != null) { + Set requestedScopes = clientRegistrationAuthentication.getClientRegistration().getScopes(); + if (!CollectionUtils.isEmpty(requestedScopes) && !this.allowedScopes.containsAll(requestedScopes)) { + throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_SCOPE, + OAuth2ClientMetadataClaimNames.SCOPE); + } + } + if (this.logger.isTraceEnabled()) { this.logger.trace("Validated client registration request parameters"); } diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java index 1d670fc4049..f4f3d49df78 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java @@ -103,6 +103,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe private PasswordEncoder passwordEncoder; + private Set allowedScopes; + /** * Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the * provided parameters. @@ -208,6 +210,18 @@ public void setPasswordEncoder(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } + /** + * Sets the allowed scopes for client registration. When set, only the specified + * scopes will be accepted during Dynamic Client Registration. If not set, any scope + * value will be accepted. + * @param allowedScopes the {@code Set} of allowed scopes + * @since 7.1 + */ + public void setAllowedScopes(Set allowedScopes) { + Assert.notNull(allowedScopes, "allowedScopes cannot be null"); + this.allowedScopes = allowedScopes; + } + private OidcClientRegistrationAuthenticationToken registerClient( OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication, OAuth2Authorization authorization) { @@ -234,6 +248,14 @@ private OidcClientRegistrationAuthenticationToken registerClient( OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD); } + if (this.allowedScopes != null) { + Set requestedScopes = clientRegistrationRequest.getScopes(); + if (!CollectionUtils.isEmpty(requestedScopes) && !this.allowedScopes.containsAll(requestedScopes)) { + throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_SCOPE, + OidcClientMetadataClaimNames.SCOPE); + } + } + if (this.logger.isTraceEnabled()) { this.logger.trace("Validated client registration request parameters"); } diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationProviderTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationProviderTests.java index b4fdbf89544..d7fde5ddb3c 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationProviderTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationProviderTests.java @@ -478,6 +478,122 @@ private static void assertClientRegistration(OAuth2ClientRegistration clientRegi .isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue()); } + @Test + public void setAllowedScopesWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.authenticationProvider.setAllowedScopes(null)) + .withMessage("allowedScopes cannot be null"); + } + + @Test + public void authenticateWhenAllowedScopesSetAndRequestedScopesAllowedThenSuccess() { + Jwt jwt = createJwtClientRegistration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, jwtAccessToken, jwt.getClaims()) + .build(); + given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()), + eq(OAuth2TokenType.ACCESS_TOKEN))) + .willReturn(authorization); + + this.authenticationProvider.setAllowedScopes(new HashSet<>(Arrays.asList("openid", "profile", "email"))); + + JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt, + AuthorityUtils.createAuthorityList("SCOPE_client.create")); + // @formatter:off + OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .scope("openid") + .scope("profile") + .build(); + // @formatter:on + + OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken( + principal, clientRegistration); + OAuth2ClientRegistrationAuthenticationToken authenticationResult = (OAuth2ClientRegistrationAuthenticationToken) this.authenticationProvider + .authenticate(authentication); + + assertThat(authenticationResult.getClientRegistration()).isNotNull(); + } + + @Test + public void authenticateWhenAllowedScopesSetAndRequestedScopesNotAllowedThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientRegistration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, jwtAccessToken, jwt.getClaims()) + .build(); + given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()), + eq(OAuth2TokenType.ACCESS_TOKEN))) + .willReturn(authorization); + + this.authenticationProvider.setAllowedScopes(new HashSet<>(Arrays.asList("openid", "profile"))); + + JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt, + AuthorityUtils.createAuthorityList("SCOPE_client.create")); + // @formatter:off + OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .scope("admin") + .scope("ROLE_ADMIN") + .build(); + // @formatter:on + + OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken( + principal, clientRegistration); + + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .extracting(OAuth2AuthenticationException::getError) + .satisfies((error) -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE); + assertThat(error.getDescription()).contains(OAuth2ClientMetadataClaimNames.SCOPE); + }); + } + + @Test + public void authenticateWhenAllowedScopesNotSetThenAnyScopeAllowed() { + Jwt jwt = createJwtClientRegistration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, jwtAccessToken, jwt.getClaims()) + .build(); + given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()), + eq(OAuth2TokenType.ACCESS_TOKEN))) + .willReturn(authorization); + + JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt, + AuthorityUtils.createAuthorityList("SCOPE_client.create")); + // @formatter:off + OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .scope("admin") + .scope("ROLE_ADMIN") + .scope("superuser") + .build(); + // @formatter:on + + OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken( + principal, clientRegistration); + OAuth2ClientRegistrationAuthenticationToken authenticationResult = (OAuth2ClientRegistrationAuthenticationToken) this.authenticationProvider + .authenticate(authentication); + + assertThat(authenticationResult.getClientRegistration()).isNotNull(); + assertThat(authenticationResult.getClientRegistration().getScopes()).containsExactlyInAnyOrder("admin", + "ROLE_ADMIN", "superuser"); + } + private static Jwt createJwtClientRegistration() { return createJwt(Collections.singleton("client.create")); } diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java index 9e73093ce6a..34f9389199f 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java @@ -766,6 +766,124 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { assertThat(clientRegistrationResult.getRegistrationAccessToken()).isEqualTo(jwt.getTokenValue()); } + @Test + public void setAllowedScopesWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.authenticationProvider.setAllowedScopes(null)) + .withMessage("allowedScopes cannot be null"); + } + + @Test + public void authenticateWhenAllowedScopesSetAndRequestedScopesAllowedThenSuccess() { + Jwt jwt = createJwtClientRegistration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, jwtAccessToken, jwt.getClaims()) + .build(); + given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()), + eq(OAuth2TokenType.ACCESS_TOKEN))) + .willReturn(authorization); + given(this.jwtEncoder.encode(any())).willReturn(createJwtClientConfiguration()); + + this.authenticationProvider.setAllowedScopes(new HashSet<>(Arrays.asList("openid", "profile", "email"))); + + JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt, + AuthorityUtils.createAuthorityList("SCOPE_client.create")); + // @formatter:off + OidcClientRegistration clientRegistration = OidcClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .scope("openid") + .scope("profile") + .build(); + // @formatter:on + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( + principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authenticationResult = (OidcClientRegistrationAuthenticationToken) this.authenticationProvider + .authenticate(authentication); + + assertThat(authenticationResult.getClientRegistration()).isNotNull(); + } + + @Test + public void authenticateWhenAllowedScopesSetAndRequestedScopesNotAllowedThenThrowOAuth2AuthenticationException() { + Jwt jwt = createJwtClientRegistration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, jwtAccessToken, jwt.getClaims()) + .build(); + given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()), + eq(OAuth2TokenType.ACCESS_TOKEN))) + .willReturn(authorization); + + this.authenticationProvider.setAllowedScopes(new HashSet<>(Arrays.asList("openid", "profile"))); + + JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt, + AuthorityUtils.createAuthorityList("SCOPE_client.create")); + // @formatter:off + OidcClientRegistration clientRegistration = OidcClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .scope("admin") + .scope("ROLE_ADMIN") + .build(); + // @formatter:on + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( + principal, clientRegistration); + + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .extracting(OAuth2AuthenticationException::getError) + .satisfies((error) -> { + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE); + assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.SCOPE); + }); + } + + @Test + public void authenticateWhenAllowedScopesNotSetThenAnyScopeAllowed() { + Jwt jwt = createJwtClientRegistration(); + OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE)); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, jwtAccessToken, jwt.getClaims()) + .build(); + given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()), + eq(OAuth2TokenType.ACCESS_TOKEN))) + .willReturn(authorization); + given(this.jwtEncoder.encode(any())).willReturn(createJwtClientConfiguration()); + + JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt, + AuthorityUtils.createAuthorityList("SCOPE_client.create")); + // @formatter:off + OidcClientRegistration clientRegistration = OidcClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .scope("admin") + .scope("ROLE_ADMIN") + .scope("superuser") + .build(); + // @formatter:on + + OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken( + principal, clientRegistration); + OidcClientRegistrationAuthenticationToken authenticationResult = (OidcClientRegistrationAuthenticationToken) this.authenticationProvider + .authenticate(authentication); + + assertThat(authenticationResult.getClientRegistration()).isNotNull(); + assertThat(authenticationResult.getClientRegistration().getScopes()).containsExactlyInAnyOrder("admin", + "ROLE_ADMIN", "superuser"); + } + private static Jwt createJwtClientRegistration() { return createJwt(Collections.singleton("client.create")); }