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")); }