Skip to content

Commit 49141b0

Browse files
committed
Add client_credentials grant type support
1 parent c803eec commit 49141b0

File tree

12 files changed

+654
-19
lines changed

12 files changed

+654
-19
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
2727
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
2828
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
29+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
2930
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
3031
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
3132
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
@@ -88,7 +89,12 @@ public void init(B builder) {
8889
new OAuth2AuthorizationCodeAuthenticationProvider(
8990
getRegisteredClientRepository(builder),
9091
getAuthorizationService(builder));
92+
93+
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider
94+
= new OAuth2ClientCredentialsAuthenticationProvider();
95+
9196
builder.authenticationProvider(postProcess(authorizationCodeAuthenticationProvider));
97+
builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
9298
}
9399

94100
@Override

core/spring-authorization-server-core.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies {
1717
testCompile 'org.assertj:assertj-core'
1818
testCompile 'org.mockito:mockito-core'
1919
testCompile 'com.squareup.okhttp3:mockwebserver'
20+
testCompile 'com.jayway.jsonpath:json-path'
2021

2122
provided 'javax.servlet:javax.servlet-api'
2223
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2020 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 java.time.Instant;
19+
import java.time.temporal.ChronoUnit;
20+
import java.util.Base64;
21+
import java.util.Set;
22+
23+
import org.springframework.security.authentication.AuthenticationProvider;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.core.AuthenticationException;
26+
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
27+
import org.springframework.security.crypto.keygen.StringKeyGenerator;
28+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
29+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
30+
import org.springframework.security.oauth2.core.OAuth2Error;
31+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
32+
33+
/**
34+
* An {@link AuthenticationProvider} that converts give {@link OAuth2ClientAuthenticationToken} to a {@link OAuth2AccessTokenAuthenticationToken}
35+
* using the provided token generator.
36+
*
37+
* @author Alexey Nesterov
38+
*/
39+
public class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
40+
41+
private final StringKeyGenerator accessTokenGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
42+
43+
@Override
44+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
45+
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthenticationToken =
46+
(OAuth2ClientCredentialsAuthenticationToken) authentication;
47+
48+
OAuth2ClientAuthenticationToken clientPrincipal = null;
49+
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(clientCredentialsAuthenticationToken.getPrincipal().getClass())) {
50+
clientPrincipal = (OAuth2ClientAuthenticationToken) clientCredentialsAuthenticationToken.getPrincipal();
51+
}
52+
53+
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
54+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
55+
}
56+
57+
Set<String> clientScopes = clientPrincipal.getRegisteredClient().getScopes();
58+
Set<String> requestedScopes = clientCredentialsAuthenticationToken.getScopes();
59+
if (!clientScopes.containsAll(requestedScopes)) {
60+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
61+
}
62+
63+
if (requestedScopes == null || requestedScopes.isEmpty()) {
64+
requestedScopes = clientScopes;
65+
}
66+
67+
String tokenValue = this.accessTokenGenerator.generateKey();
68+
Instant issuedAt = Instant.now();
69+
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); // TODO Allow configuration for access token lifespan
70+
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
71+
tokenValue, issuedAt, expiresAt, requestedScopes);
72+
73+
return new OAuth2AccessTokenAuthenticationToken(
74+
clientPrincipal.getRegisteredClient(), clientPrincipal, accessToken);
75+
}
76+
77+
@Override
78+
public boolean supports(Class<?> authentication) {
79+
return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2020 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+
17+
package org.springframework.security.oauth2.server.authorization.authentication;
18+
19+
import java.util.Collections;
20+
import java.util.Set;
21+
22+
import org.springframework.security.authentication.AbstractAuthenticationToken;
23+
import org.springframework.security.core.Authentication;
24+
import org.springframework.util.Assert;
25+
26+
public class OAuth2ClientCredentialsAuthenticationToken extends AbstractAuthenticationToken {
27+
28+
private final Authentication clientPrincipal;
29+
private final Set<String> scopes;
30+
31+
public OAuth2ClientCredentialsAuthenticationToken(OAuth2ClientAuthenticationToken clientPrincipal, Set<String> scopes) {
32+
super(Collections.emptyList());
33+
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
34+
Assert.notNull(scopes, "scopes cannot be null");
35+
this.clientPrincipal = clientPrincipal;
36+
this.scopes = scopes;
37+
}
38+
39+
@SuppressWarnings("unchecked")
40+
public OAuth2ClientCredentialsAuthenticationToken(OAuth2ClientAuthenticationToken clientPrincipal) {
41+
this(clientPrincipal, Collections.EMPTY_SET);
42+
}
43+
44+
@Override
45+
public Object getCredentials() {
46+
return "";
47+
}
48+
49+
@Override
50+
public Object getPrincipal() {
51+
return this.clientPrincipal;
52+
}
53+
54+
public Set<String> getScopes() {
55+
return this.scopes;
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2020 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+
17+
package org.springframework.security.oauth2.server.authorization.web;
18+
19+
import javax.servlet.http.HttpServletRequest;
20+
import java.util.Collections;
21+
import java.util.Map;
22+
23+
import org.springframework.core.convert.converter.Converter;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
26+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
27+
import org.springframework.util.StringUtils;
28+
29+
public final class DelegatingAuthorizationGrantAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
30+
31+
private final Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters;
32+
33+
DelegatingAuthorizationGrantAuthenticationConverter(Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters) {
34+
this.converters = Collections.unmodifiableMap(converters);
35+
}
36+
37+
@Override
38+
public Authentication convert(HttpServletRequest request) {
39+
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
40+
if (StringUtils.isEmpty(grantType)) {
41+
return null;
42+
}
43+
44+
Converter<HttpServletRequest, Authentication> converter = this.converters.get(new AuthorizationGrantType(grantType));
45+
if (converter == null) {
46+
return null;
47+
}
48+
49+
return converter.convert(request);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2020 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+
17+
package org.springframework.security.oauth2.server.authorization.web;
18+
19+
import java.util.Arrays;
20+
import java.util.HashSet;
21+
import java.util.Set;
22+
23+
/**
24+
* Parses scope parameter according to 3.3 section of the spec.
25+
*
26+
* @see <a href="https://tools.ietf.org/html/rfc6749#section-3.3" target="_blank">3.3. Access Token Scope</a>
27+
*/
28+
class OAuth2ScopeParser {
29+
30+
// scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
31+
// scope = scope-token *( SP scope-token )
32+
private static final String SCOPE_TOKEN = "[\\x21\\x23-\\x5B\\x5D-\\x7E]+";
33+
private static final String SCOPE = "^" + SCOPE_TOKEN + "(?:\\ " + SCOPE_TOKEN + ")*$";
34+
35+
static class InvalidScopeFormatException extends Exception {
36+
}
37+
38+
Set<String> parse(String scope) throws InvalidScopeFormatException {
39+
if (!scope.matches(SCOPE)) {
40+
throw new InvalidScopeFormatException();
41+
}
42+
43+
String[] scopes = scope.split(" ");
44+
return new HashSet<>(Arrays.asList(scopes));
45+
}
46+
}

core/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java

+60-12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.web;
1717

18+
import javax.servlet.FilterChain;
19+
import javax.servlet.ServletException;
20+
import javax.servlet.http.HttpServletRequest;
21+
import javax.servlet.http.HttpServletResponse;
22+
import java.io.IOException;
23+
import java.time.temporal.ChronoUnit;
24+
import java.util.HashMap;
25+
import java.util.Map;
26+
import java.util.Set;
27+
1828
import org.springframework.core.convert.converter.Converter;
1929
import org.springframework.http.HttpMethod;
2030
import org.springframework.http.HttpStatus;
@@ -35,20 +45,15 @@
3545
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
3646
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
3747
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
48+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
49+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
3850
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
3951
import org.springframework.security.web.util.matcher.RequestMatcher;
4052
import org.springframework.util.Assert;
4153
import org.springframework.util.MultiValueMap;
4254
import org.springframework.util.StringUtils;
4355
import org.springframework.web.filter.OncePerRequestFilter;
4456

45-
import javax.servlet.FilterChain;
46-
import javax.servlet.ServletException;
47-
import javax.servlet.http.HttpServletRequest;
48-
import javax.servlet.http.HttpServletResponse;
49-
import java.io.IOException;
50-
import java.time.temporal.ChronoUnit;
51-
5257
/**
5358
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
5459
* which handles the processing of the OAuth 2.0 Access Token Request.
@@ -86,8 +91,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
8691
private final AuthenticationManager authenticationManager;
8792
private final OAuth2AuthorizationService authorizationService;
8893
private final RequestMatcher tokenEndpointMatcher;
89-
private final Converter<HttpServletRequest, Authentication> authorizationGrantAuthenticationConverter =
90-
new AuthorizationCodeAuthenticationConverter();
94+
private final Converter<HttpServletRequest, Authentication> authorizationGrantAuthenticationConverter;
95+
9196
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
9297
new OAuth2AccessTokenResponseHttpMessageConverter();
9398
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
@@ -119,6 +124,11 @@ public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager,
119124
this.authenticationManager = authenticationManager;
120125
this.authorizationService = authorizationService;
121126
this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
127+
128+
Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters = new HashMap<>();
129+
converters.put(AuthorizationGrantType.AUTHORIZATION_CODE, new AuthorizationCodeAuthenticationConverter());
130+
converters.put(AuthorizationGrantType.CLIENT_CREDENTIALS, new ClientCredentialsAuthenticationConverter());
131+
this.authorizationGrantAuthenticationConverter = new DelegatingAuthorizationGrantAuthenticationConverter(converters);
122132
}
123133

124134
@Override
@@ -131,8 +141,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
131141
}
132142

133143
try {
134-
Authentication authorizationGrantAuthentication =
135-
this.authorizationGrantAuthenticationConverter.convert(request);
144+
Authentication authorizationGrantAuthentication = this.authorizationGrantAuthenticationConverter.convert(request);
145+
if (authorizationGrantAuthentication == null) {
146+
throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, "grant_type");
147+
}
148+
136149
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
137150
(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);
138151
sendAccessTokenResponse(response, accessTokenAuthentication.getAccessToken());
@@ -161,7 +174,7 @@ private void sendErrorResponse(HttpServletResponse response, OAuth2Error error)
161174
this.errorHttpResponseConverter.write(error, null, httpResponse);
162175
}
163176

164-
private static OAuth2AuthenticationException throwError(String errorCode, String parameterName) {
177+
private static void throwError(String errorCode, String parameterName) {
165178
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName,
166179
"https://tools.ietf.org/html/rfc6749#section-5.2");
167180
throw new OAuth2AuthenticationException(error);
@@ -214,4 +227,39 @@ public Authentication convert(HttpServletRequest request) {
214227
new OAuth2AuthorizationCodeAuthenticationToken(code, clientId, redirectUri);
215228
}
216229
}
230+
231+
/**
232+
* Validates that client is authenticated by {@link OAuth2ClientAuthenticationFilter}
233+
*
234+
* @see OAuth2ClientAuthenticationToken
235+
* @see OAuth2ClientAuthenticationFilter
236+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.4">Section 4.4 Client Credentials Grant</a>
237+
*/
238+
private static class ClientCredentialsAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
239+
240+
private static final String SCOPE_PARAMETER_NAME = OAuth2ParameterNames.SCOPE;
241+
private final OAuth2ScopeParser scopesParser = new OAuth2ScopeParser();
242+
243+
@Override
244+
public Authentication convert(HttpServletRequest request) {
245+
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
246+
final OAuth2ClientAuthenticationToken clientAuthenticationToken = (OAuth2ClientAuthenticationToken) authentication;
247+
248+
// "scope" is optional, see https://tools.ietf.org/html/rfc6749#section-4.4.2 and https://tools.ietf.org/html/rfc6749#section-3.3
249+
String scopeParameter = request.getParameter(SCOPE_PARAMETER_NAME);
250+
if (StringUtils.isEmpty(scopeParameter)) {
251+
return new OAuth2ClientCredentialsAuthenticationToken(clientAuthenticationToken);
252+
}
253+
254+
Set<String> requestedScopes = null;
255+
try {
256+
requestedScopes = scopesParser.parse(scopeParameter);
257+
}
258+
catch (OAuth2ScopeParser.InvalidScopeFormatException e) {
259+
throwError(OAuth2ErrorCodes.INVALID_SCOPE, "scope");
260+
}
261+
262+
return new OAuth2ClientCredentialsAuthenticationToken(clientAuthenticationToken, requestedScopes);
263+
}
264+
}
217265
}

0 commit comments

Comments
 (0)