Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,6 +77,8 @@ public final class OAuth2ClientRegistrationEndpointConfigurer extends AbstractOA

private boolean openRegistrationAllowed;

private Set<String> allowedScopes;

/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
Expand Down Expand Up @@ -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
Expand All @@ -207,7 +227,7 @@ void init(HttpSecurity httpSecurity) {
.matcher(HttpMethod.POST, clientRegistrationEndpointUri);

List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity,
this.openRegistrationAllowed);
this.openRegistrationAllowed, this.allowedScopes);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
Expand Down Expand Up @@ -258,7 +278,7 @@ private static List<AuthenticationConverter> createDefaultAuthenticationConverte
}

private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity,
boolean openRegistrationAllowed) {
boolean openRegistrationAllowed, Set<String> allowedScopes) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();

OAuth2ClientRegistrationAuthenticationProvider clientRegistrationAuthenticationProvider = new OAuth2ClientRegistrationAuthenticationProvider(
Expand All @@ -269,6 +289,9 @@ private static List<AuthenticationProvider> createDefaultAuthenticationProviders
clientRegistrationAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
clientRegistrationAuthenticationProvider.setOpenRegistrationAllowed(openRegistrationAllowed);
if (allowedScopes != null) {
clientRegistrationAuthenticationProvider.setAllowedScopes(allowedScopes);
}
authenticationProviders.add(clientRegistrationAuthenticationProvider);

return authenticationProviders;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,6 +78,8 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut

private AuthenticationFailureHandler errorResponseHandler;

private Set<String> allowedScopes;

/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
Expand Down Expand Up @@ -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
Expand All @@ -194,7 +214,8 @@ void init(HttpSecurity httpSecurity) {
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, clientRegistrationEndpointUri),
PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, clientRegistrationEndpointUri));

List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity,
this.allowedScopes);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
Expand Down Expand Up @@ -245,7 +266,8 @@ private static List<AuthenticationConverter> createDefaultAuthenticationConverte
return authenticationConverters;
}

private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity,
Set<String> allowedScopes) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();

OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider = new OidcClientRegistrationAuthenticationProvider(
Expand All @@ -256,6 +278,9 @@ private static List<AuthenticationProvider> createDefaultAuthenticationProviders
if (passwordEncoder != null) {
oidcClientRegistrationAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
if (allowedScopes != null) {
oidcClientRegistrationAuthenticationProvider.setAllowedScopes(allowedScopes);
}
authenticationProviders.add(oidcClientRegistrationAuthenticationProvider);

OidcClientConfigurationAuthenticationProvider oidcClientConfigurationAuthenticationProvider = new OidcClientConfigurationAuthenticationProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<OAuth2ClientRegistration, RegisteredClient>` that performs the required validation.

[[oauth2AuthorizationServer-oauth2-authorization-server-metadata-endpoint]]
== OAuth2 Authorization Server Metadata Endpoint

Expand Down Expand Up @@ -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<OidcClientRegistration, RegisteredClient>` that performs the required validation.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut

private boolean openRegistrationAllowed;

private Set<String> allowedScopes;

/**
* Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the
* provided parameters.
Expand Down Expand Up @@ -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<String> allowedScopes) {
Assert.notNull(allowedScopes, "allowedScopes cannot be null");
this.allowedScopes = allowedScopes;
}

private OAuth2ClientRegistrationAuthenticationToken registerClient(
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication,
@Nullable OAuth2Authorization authorization) {
Expand All @@ -210,6 +224,14 @@ private OAuth2ClientRegistrationAuthenticationToken registerClient(
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}

if (this.allowedScopes != null) {
Set<String> 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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe

private PasswordEncoder passwordEncoder;

private Set<String> allowedScopes;

/**
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the
* provided parameters.
Expand Down Expand Up @@ -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<String> allowedScopes) {
Assert.notNull(allowedScopes, "allowedScopes cannot be null");
this.allowedScopes = allowedScopes;
}

private OidcClientRegistrationAuthenticationToken registerClient(
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
OAuth2Authorization authorization) {
Expand All @@ -234,6 +248,14 @@ private OidcClientRegistrationAuthenticationToken registerClient(
OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
}

if (this.allowedScopes != null) {
Set<String> 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");
}
Expand Down
Loading