Skip to content

Commit 23ffd28

Browse files
committed
Add client_credentials grant type support
1 parent 02b64f0 commit 23ffd28

File tree

10 files changed

+555
-5
lines changed

10 files changed

+555
-5
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+
}

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

+50-5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
3636
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
3737
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
38+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
39+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
3840
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
3941
import org.springframework.security.web.util.matcher.RequestMatcher;
4042
import org.springframework.util.Assert;
@@ -48,6 +50,11 @@
4850
import javax.servlet.http.HttpServletResponse;
4951
import java.io.IOException;
5052
import java.time.temporal.ChronoUnit;
53+
import java.util.Arrays;
54+
import java.util.HashMap;
55+
import java.util.HashSet;
56+
import java.util.Map;
57+
import java.util.Set;
5158

5259
/**
5360
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
@@ -86,8 +93,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
8693
private final AuthenticationManager authenticationManager;
8794
private final OAuth2AuthorizationService authorizationService;
8895
private final RequestMatcher tokenEndpointMatcher;
89-
private final Converter<HttpServletRequest, Authentication> authorizationGrantAuthenticationConverter =
90-
new AuthorizationCodeAuthenticationConverter();
96+
private final Converter<HttpServletRequest, Authentication> authorizationGrantAuthenticationConverter;
97+
9198
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
9299
new OAuth2AccessTokenResponseHttpMessageConverter();
93100
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
@@ -119,6 +126,11 @@ public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager,
119126
this.authenticationManager = authenticationManager;
120127
this.authorizationService = authorizationService;
121128
this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
129+
130+
Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters = new HashMap<>();
131+
converters.put(AuthorizationGrantType.AUTHORIZATION_CODE, new AuthorizationCodeAuthenticationConverter());
132+
converters.put(AuthorizationGrantType.CLIENT_CREDENTIALS, new ClientCredentialsAuthenticationConverter());
133+
this.authorizationGrantAuthenticationConverter = new DelegatingAuthorizationGrantAuthenticationConverter(converters);
122134
}
123135

124136
@Override
@@ -131,8 +143,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
131143
}
132144

133145
try {
134-
Authentication authorizationGrantAuthentication =
135-
this.authorizationGrantAuthenticationConverter.convert(request);
146+
String[] grantTypes = request.getParameterValues(OAuth2ParameterNames.GRANT_TYPE);
147+
if (grantTypes == null || grantTypes.length == 0) {
148+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "grant_type");
149+
}
150+
151+
Authentication authorizationGrantAuthentication = this.authorizationGrantAuthenticationConverter.convert(request);
152+
if (authorizationGrantAuthentication == null) {
153+
throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, "grant_type");
154+
}
155+
136156
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
137157
(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);
138158
sendAccessTokenResponse(response, accessTokenAuthentication.getAccessToken());
@@ -161,7 +181,7 @@ private void sendErrorResponse(HttpServletResponse response, OAuth2Error error)
161181
this.errorHttpResponseConverter.write(error, null, httpResponse);
162182
}
163183

164-
private static OAuth2AuthenticationException throwError(String errorCode, String parameterName) {
184+
private static void throwError(String errorCode, String parameterName) {
165185
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName,
166186
"https://tools.ietf.org/html/rfc6749#section-5.2");
167187
throw new OAuth2AuthenticationException(error);
@@ -214,4 +234,29 @@ public Authentication convert(HttpServletRequest request) {
214234
new OAuth2AuthorizationCodeAuthenticationToken(code, clientId, redirectUri);
215235
}
216236
}
237+
238+
private static class ClientCredentialsAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
239+
240+
@Override
241+
public Authentication convert(HttpServletRequest request) {
242+
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
243+
final OAuth2ClientAuthenticationToken clientAuthenticationToken = (OAuth2ClientAuthenticationToken) authentication;
244+
245+
// grant_type (REQUIRED)
246+
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
247+
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) {
248+
throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, OAuth2ParameterNames.GRANT_TYPE);
249+
}
250+
251+
// scope (OPTIONAL)
252+
// https://tools.ietf.org/html/rfc6749#section-4.4.2
253+
String scopeParameter = request.getParameter(OAuth2ParameterNames.SCOPE);
254+
if (StringUtils.isEmpty(scopeParameter)) {
255+
return new OAuth2ClientCredentialsAuthenticationToken(clientAuthenticationToken);
256+
}
257+
258+
Set<String> requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scopeParameter, " ")));
259+
return new OAuth2ClientCredentialsAuthenticationToken(clientAuthenticationToken, requestedScopes);
260+
}
261+
}
217262
}

0 commit comments

Comments
 (0)