Skip to content

Commit 5c3f1cb

Browse files
adamleantechjgrandja
authored andcommitted
Allow configurable scope validation strategy in OAuth2ClientCredentialsAuthenticationProvider
Closes gh-1377
1 parent 168077b commit 5c3f1cb

File tree

10 files changed

+480
-43
lines changed

10 files changed

+480
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2020-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import org.springframework.lang.Nullable;
19+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
20+
import org.springframework.util.Assert;
21+
22+
import java.util.Map;
23+
import java.util.function.Consumer;
24+
25+
/**
26+
* An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2ClientCredentialsAuthenticationToken} and additional information
27+
* and is used when validating the OAuth 2.0 Authorization Request used in the Client Credentials Grant.
28+
*
29+
* @author Adam Pilling
30+
* @since 1.3.0
31+
* @see OAuth2AuthenticationContext
32+
* @see OAuth2ClientCredentialsAuthenticationToken
33+
* @see OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer)
34+
*/
35+
public final class OAuth2ClientCredentialsAuthenticationContext implements OAuth2AuthenticationContext {
36+
private final Map<Object, Object> context;
37+
38+
private OAuth2ClientCredentialsAuthenticationContext(Map<Object, Object> context) {
39+
this.context = Map.copyOf(context);
40+
}
41+
42+
@SuppressWarnings("unchecked")
43+
@Nullable
44+
@Override
45+
public <V> V get(Object key) {
46+
return hasKey(key) ? (V) this.context.get(key) : null;
47+
}
48+
49+
@Override
50+
public boolean hasKey(Object key) {
51+
Assert.notNull(key, "key cannot be null");
52+
return this.context.containsKey(key);
53+
}
54+
55+
/**
56+
* Returns the {@link RegisteredClient registered client}.
57+
*
58+
* @return the {@link RegisteredClient}
59+
*/
60+
public RegisteredClient getRegisteredClient() {
61+
return get(RegisteredClient.class);
62+
}
63+
64+
/**
65+
* Constructs a new {@link Builder} with the provided {@link OAuth2ClientCredentialsAuthenticationToken}.
66+
*
67+
* @param authentication the {@link OAuth2ClientCredentialsAuthenticationToken}
68+
* @return the {@link Builder}
69+
*/
70+
public static Builder with(OAuth2ClientCredentialsAuthenticationToken authentication) {
71+
return new Builder(authentication);
72+
}
73+
74+
/**
75+
* A builder for {@link OAuth2ClientCredentialsAuthenticationContext}.
76+
*/
77+
public static final class Builder extends AbstractBuilder<OAuth2ClientCredentialsAuthenticationContext, Builder> {
78+
79+
private Builder(OAuth2ClientCredentialsAuthenticationToken authentication) {
80+
super(authentication);
81+
}
82+
83+
/**
84+
* Sets the {@link RegisteredClient registered client}.
85+
*
86+
* @param registeredClient the {@link RegisteredClient}
87+
* @return the {@link Builder} for further configuration
88+
*/
89+
public Builder registeredClient(RegisteredClient registeredClient) {
90+
return put(RegisteredClient.class, registeredClient);
91+
}
92+
93+
/**
94+
* Builds a new {@link OAuth2ClientCredentialsAuthenticationContext}.
95+
*
96+
* @return the {@link OAuth2ClientCredentialsAuthenticationContext}
97+
*/
98+
public OAuth2ClientCredentialsAuthenticationContext build() {
99+
Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
100+
return new OAuth2ClientCredentialsAuthenticationContext(getContext());
101+
}
102+
103+
}
104+
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2020-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import org.springframework.lang.Nullable;
19+
import org.springframework.security.core.Authentication;
20+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
21+
import org.springframework.security.oauth2.core.OAuth2Error;
22+
23+
/**
24+
* This exception is thrown by {@link OAuth2ClientCredentialsAuthenticationProvider}
25+
* when an attempt to authenticate the OAuth 2.0 Authorization Request (or Consent) fails.
26+
*
27+
* @author Adam Pilling
28+
* @since 1.3.0
29+
* @see OAuth2ClientCredentialsAuthenticationToken
30+
* @see OAuth2ClientCredentialsAuthenticationProvider
31+
*/
32+
public class OAuth2ClientCredentialsAuthenticationException extends OAuth2AuthenticationException {
33+
private final OAuth2ClientCredentialsAuthenticationToken authorizationCodeRequestAuthentication;
34+
35+
/**
36+
* Constructs an {@code OAuth2ClientCredentialsAuthenticationException} using the provided parameters.
37+
*
38+
* @param error the {@link OAuth2Error OAuth 2.0 Error}
39+
* @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
40+
*/
41+
public OAuth2ClientCredentialsAuthenticationException(
42+
OAuth2Error error,
43+
@Nullable OAuth2ClientCredentialsAuthenticationToken authorizationCodeRequestAuthentication) {
44+
super(error);
45+
this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
46+
}
47+
48+
/**
49+
* Returns the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent), or {@code null} if not available.
50+
*
51+
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
52+
*/
53+
@Nullable
54+
public OAuth2ClientCredentialsAuthenticationToken getClientCredentialsAuthentication() {
55+
return this.authorizationCodeRequestAuthentication;
56+
}
57+
58+
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,8 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.authentication;
1717

18-
import java.util.Collections;
19-
import java.util.LinkedHashSet;
20-
import java.util.Set;
21-
2218
import org.apache.commons.logging.Log;
2319
import org.apache.commons.logging.LogFactory;
24-
2520
import org.springframework.security.authentication.AuthenticationProvider;
2621
import org.springframework.security.core.Authentication;
2722
import org.springframework.security.core.AuthenticationException;
@@ -41,7 +36,9 @@
4136
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
4237
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
4338
import org.springframework.util.Assert;
44-
import org.springframework.util.CollectionUtils;
39+
40+
import java.util.Set;
41+
import java.util.function.Consumer;
4542

4643
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
4744

@@ -63,6 +60,8 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
6360
private final Log logger = LogFactory.getLog(getClass());
6461
private final OAuth2AuthorizationService authorizationService;
6562
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
63+
private Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator =
64+
new OAuth2ClientCredentialsAuthenticationValidator();
6665

6766
/**
6867
* Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters.
@@ -96,20 +95,18 @@ public Authentication authenticate(Authentication authentication) throws Authent
9695
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
9796
}
9897

99-
Set<String> authorizedScopes = Collections.emptySet();
100-
if (!CollectionUtils.isEmpty(clientCredentialsAuthentication.getScopes())) {
101-
for (String requestedScope : clientCredentialsAuthentication.getScopes()) {
102-
if (!registeredClient.getScopes().contains(requestedScope)) {
103-
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
104-
}
105-
}
106-
authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
107-
}
98+
OAuth2ClientCredentialsAuthenticationContext authenticationContext =
99+
OAuth2ClientCredentialsAuthenticationContext.with(clientCredentialsAuthentication)
100+
.registeredClient(registeredClient)
101+
.build();
102+
authenticationValidator.accept(authenticationContext);
108103

109104
if (this.logger.isTraceEnabled()) {
110105
this.logger.trace("Validated token request parameters");
111106
}
112107

108+
Set<String> authorizedScopes = Set.copyOf(clientCredentialsAuthentication.getScopes());
109+
113110
// @formatter:off
114111
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
115112
.registeredClient(registeredClient)
@@ -168,4 +165,22 @@ public boolean supports(Class<?> authentication) {
168165
return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
169166
}
170167

168+
/**
169+
* Sets the {@code Consumer} providing access to the {@link OAuth2ClientCredentialsAuthenticationContext}
170+
* and is responsible for validating specific OAuth 2.0 Client Credentials parameters
171+
* associated in the {@link OAuth2ClientCredentialsAuthenticationToken}.
172+
* The default authentication validator is {@link OAuth2ClientCredentialsAuthenticationValidator}.
173+
*
174+
* <p>
175+
* <b>NOTE:</b> The authentication validator MUST throw {@link OAuth2ClientCredentialsAuthenticationException} if validation fails.
176+
*
177+
* @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2ClientCredentialsAuthenticationContext}
178+
* and is responsible for validating specific OAuth 2.0 Authorization Request parameters
179+
* @since 1.3.0
180+
*/
181+
public void setAuthenticationValidator(Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator) {
182+
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
183+
this.authenticationValidator = authenticationValidator;
184+
}
185+
171186
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import org.apache.commons.logging.Log;
19+
import org.apache.commons.logging.LogFactory;
20+
import org.springframework.core.log.LogMessage;
21+
import org.springframework.security.core.Authentication;
22+
import org.springframework.security.oauth2.core.OAuth2Error;
23+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
24+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
25+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
26+
27+
import java.util.Set;
28+
import java.util.function.Consumer;
29+
30+
/**
31+
* A {@code Consumer} providing access to the {@link OAuth2ClientCredentialsAuthenticationContext}
32+
* containing an {@link OAuth2ClientCredentialsAuthenticationToken}
33+
* and is the default {@link OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer) authentication validator}
34+
* used for validating specific OAuth 2.0 Client Credentials parameters used in the Client Credentials Grant.
35+
*
36+
* <p>
37+
* The default compares the provided scopes with those configured in the RegisteredClient.
38+
* If validation fails, an {@link OAuth2ClientCredentialsAuthenticationException} is thrown.
39+
*
40+
* @author Adam Pilling
41+
* @since 1.3.0
42+
* @see OAuth2ClientCredentialsAuthenticationContext
43+
* @see RegisteredClient
44+
* @see OAuth2ClientCredentialsAuthenticationToken
45+
* @see OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer)
46+
*/
47+
public final class OAuth2ClientCredentialsAuthenticationValidator implements Consumer<OAuth2ClientCredentialsAuthenticationContext> {
48+
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
49+
private static final Log LOGGER = LogFactory.getLog(OAuth2ClientCredentialsAuthenticationValidator.class);
50+
51+
/**
52+
* The default validator for {@link OAuth2ClientCredentialsAuthenticationToken#getScopes()}.
53+
*/
54+
public static final Consumer<OAuth2ClientCredentialsAuthenticationContext> DEFAULT_SCOPE_VALIDATOR =
55+
OAuth2ClientCredentialsAuthenticationValidator::validateScope;
56+
57+
private final Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator = DEFAULT_SCOPE_VALIDATOR;
58+
59+
@Override
60+
public void accept(OAuth2ClientCredentialsAuthenticationContext authenticationContext) {
61+
this.authenticationValidator.accept(authenticationContext);
62+
}
63+
64+
private static void validateScope(OAuth2ClientCredentialsAuthenticationContext authenticationContext) {
65+
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthenticationToken =
66+
authenticationContext.getAuthentication();
67+
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
68+
69+
Set<String> requestedScopes = clientCredentialsAuthenticationToken.getScopes();
70+
Set<String> allowedScopes = registeredClient.getScopes();
71+
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
72+
if (LOGGER.isDebugEnabled()) {
73+
LOGGER.debug(LogMessage.format("Invalid request: requested scope is not allowed" +
74+
" for registered client '%s'", registeredClient.getId()));
75+
}
76+
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, clientCredentialsAuthenticationToken);
77+
}
78+
}
79+
80+
private static void throwError(String errorCode, String parameterName,
81+
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthenticationToken) {
82+
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
83+
OAuth2ClientCredentialsAuthenticationToken authorizationCodeRequestAuthenticationResult =
84+
new OAuth2ClientCredentialsAuthenticationToken(
85+
(Authentication) clientCredentialsAuthenticationToken.getPrincipal(),
86+
clientCredentialsAuthenticationToken.getScopes(),
87+
clientCredentialsAuthenticationToken.getAdditionalParameters());
88+
authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
89+
90+
throw new OAuth2ClientCredentialsAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
91+
}
92+
93+
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.function.Consumer;
2121

2222
import jakarta.servlet.http.HttpServletRequest;
23-
2423
import org.springframework.http.HttpMethod;
2524
import org.springframework.security.authentication.AuthenticationManager;
2625
import org.springframework.security.authentication.AuthenticationProvider;
@@ -217,7 +216,7 @@ private static List<AuthenticationConverter> createDefaultAuthenticationConverte
217216
return authenticationConverters;
218217
}
219218

220-
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
219+
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
221220
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
222221

223222
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);

0 commit comments

Comments
 (0)