Skip to content

Commit acb8bea

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

File tree

12 files changed

+656
-13
lines changed

12 files changed

+656
-13
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,74 @@
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+
22+
import org.springframework.security.authentication.AuthenticationProvider;
23+
import org.springframework.security.core.Authentication;
24+
import org.springframework.security.core.AuthenticationException;
25+
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
26+
import org.springframework.security.crypto.keygen.StringKeyGenerator;
27+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
28+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
29+
import org.springframework.security.oauth2.core.OAuth2Error;
30+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
31+
32+
/**
33+
* An {@link AuthenticationProvider} that converts give {@link OAuth2ClientAuthenticationToken} to a {@link OAuth2AccessTokenAuthenticationToken}
34+
* using the provided token generator.
35+
*
36+
* @author Alexey Nesterov
37+
*/
38+
public class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
39+
40+
private final StringKeyGenerator accessTokenGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
41+
42+
public OAuth2ClientCredentialsAuthenticationProvider() {
43+
44+
}
45+
46+
@Override
47+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
48+
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthenticationToken =
49+
(OAuth2ClientCredentialsAuthenticationToken) authentication;
50+
51+
OAuth2ClientAuthenticationToken clientPrincipal = null;
52+
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(clientCredentialsAuthenticationToken.getPrincipal().getClass())) {
53+
clientPrincipal = (OAuth2ClientAuthenticationToken) clientCredentialsAuthenticationToken.getPrincipal();
54+
}
55+
56+
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
57+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
58+
}
59+
60+
String tokenValue = this.accessTokenGenerator.generateKey();
61+
Instant issuedAt = Instant.now();
62+
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); // TODO Allow configuration for access token lifespan
63+
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
64+
tokenValue, issuedAt, expiresAt);
65+
66+
return new OAuth2AccessTokenAuthenticationToken(
67+
clientPrincipal.getRegisteredClient(), clientPrincipal, accessToken);
68+
}
69+
70+
@Override
71+
public boolean supports(Class<?> authentication) {
72+
return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
73+
}
74+
}
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 scopes;
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.HashMap;
21+
22+
import org.springframework.core.convert.converter.Converter;
23+
import org.springframework.security.core.Authentication;
24+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
25+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
26+
import org.springframework.security.oauth2.core.OAuth2Error;
27+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
28+
import org.springframework.util.StringUtils;
29+
30+
public class DelegateGrantTypeAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
31+
32+
private final HashMap<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters = new HashMap<>();
33+
34+
@Override
35+
public Authentication convert(HttpServletRequest request) {
36+
String grantType = request.getParameter("grant_type");
37+
if (StringUtils.isEmpty(grantType)) {
38+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "grant_type");
39+
}
40+
41+
Converter<HttpServletRequest, Authentication> converter = this.converters.get(new AuthorizationGrantType(grantType));
42+
if (converter == null) {
43+
throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, "grant_type");
44+
}
45+
46+
return converter.convert(request);
47+
}
48+
49+
public DelegateGrantTypeAuthenticationConverter registerConverter(AuthorizationGrantType authorizationGrantType, Converter<HttpServletRequest, Authentication> authenticationConverter) {
50+
this.converters.put(authorizationGrantType, authenticationConverter);
51+
return this;
52+
}
53+
54+
private void throwError(String errorCode, String parameterName) {
55+
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName,
56+
"https://tools.ietf.org/html/rfc6749#section-5.2");
57+
throw new OAuth2AuthenticationException(error);
58+
}
59+
}
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+
}

0 commit comments

Comments
 (0)