Skip to content

Commit ed9a404

Browse files
committed
Add Refresh Token grant type support
1 parent 18f8b3a commit ed9a404

21 files changed

+1232
-78
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
3232
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
3333
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
34+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
3435
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
3536
import org.springframework.security.oauth2.server.authorization.web.JwkSetEndpointFilter;
3637
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
@@ -145,6 +146,10 @@ public void init(B builder) {
145146
jwtEncoder);
146147
builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
147148

149+
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
150+
new OAuth2RefreshTokenAuthenticationProvider(getAuthorizationService(builder), jwtEncoder);
151+
builder.authenticationProvider(postProcess(refreshTokenAuthenticationProvider));
152+
148153
ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
149154
if (exceptionHandling != null) {
150155
// Register the default AuthenticationEntryPoint for the token endpoint

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,18 @@ private boolean hasToken(OAuth2Authorization authorization, String token, TokenT
6666
} else if (TokenType.AUTHORIZATION_CODE.equals(tokenType)) {
6767
OAuth2AuthorizationCode authorizationCode = authorization.getTokens().getToken(OAuth2AuthorizationCode.class);
6868
return authorizationCode != null && authorizationCode.getTokenValue().equals(token);
69-
} else if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
69+
}
70+
71+
if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
7072
return authorization.getTokens().getAccessToken() != null &&
7173
authorization.getTokens().getAccessToken().getTokenValue().equals(token);
7274
}
75+
76+
if (TokenType.REFRESH_TOKEN.equals(tokenType)) {
77+
return authorization.getTokens().getRefreshToken() != null &&
78+
authorization.getTokens().getRefreshToken().getTokenValue().equals(token);
79+
}
80+
7381
return false;
7482
}
7583

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

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
public final class TokenType implements Serializable {
2727
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
2828
public static final TokenType ACCESS_TOKEN = new TokenType("access_token");
29+
public static final TokenType REFRESH_TOKEN = new TokenType("refresh_token");
2930
public static final TokenType AUTHORIZATION_CODE = new TokenType("authorization_code");
3031
private final String value;
3132

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

+28
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.authentication;
1717

18+
import org.springframework.lang.Nullable;
1819
import org.springframework.security.authentication.AbstractAuthenticationToken;
1920
import org.springframework.security.core.Authentication;
2021
import org.springframework.security.oauth2.server.authorization.Version;
2122
import org.springframework.security.oauth2.core.OAuth2AccessToken;
23+
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
2224
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
2325
import org.springframework.util.Assert;
2426

@@ -41,6 +43,7 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication
4143
private final RegisteredClient registeredClient;
4244
private final Authentication clientPrincipal;
4345
private final OAuth2AccessToken accessToken;
46+
private final OAuth2RefreshToken refreshToken;
4447

4548
/**
4649
* Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters.
@@ -51,13 +54,27 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication
5154
*/
5255
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient,
5356
Authentication clientPrincipal, OAuth2AccessToken accessToken) {
57+
this(registeredClient, clientPrincipal, accessToken, null);
58+
}
59+
60+
/**
61+
* Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters.
62+
*
63+
* @param registeredClient the registered client
64+
* @param clientPrincipal the authenticated client principal
65+
* @param accessToken the access token
66+
* @param refreshToken the refresh token
67+
*/
68+
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient,
69+
Authentication clientPrincipal, OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
5470
super(Collections.emptyList());
5571
Assert.notNull(registeredClient, "registeredClient cannot be null");
5672
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
5773
Assert.notNull(accessToken, "accessToken cannot be null");
5874
this.registeredClient = registeredClient;
5975
this.clientPrincipal = clientPrincipal;
6076
this.accessToken = accessToken;
77+
this.refreshToken = refreshToken;
6178
}
6279

6380
@Override
@@ -87,4 +104,15 @@ public RegisteredClient getRegisteredClient() {
87104
public OAuth2AccessToken getAccessToken() {
88105
return this.accessToken;
89106
}
107+
108+
109+
/**
110+
* Returns the {@link OAuth2RefreshToken} if provided
111+
*
112+
* @return the {@link OAuth2RefreshToken}
113+
*/
114+
@Nullable
115+
public OAuth2RefreshToken getRefreshToken() {
116+
return refreshToken;
117+
}
90118
}

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

+14-37
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,9 @@
2222
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2323
import org.springframework.security.oauth2.core.OAuth2Error;
2424
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
25+
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
2526
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
26-
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
27-
import org.springframework.security.oauth2.jose.JoseHeader;
28-
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
2927
import org.springframework.security.oauth2.jwt.Jwt;
30-
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
3128
import org.springframework.security.oauth2.jwt.JwtEncoder;
3229
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
3330
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
@@ -41,12 +38,6 @@
4138
import org.springframework.util.Assert;
4239
import org.springframework.util.StringUtils;
4340

44-
import java.net.MalformedURLException;
45-
import java.net.URI;
46-
import java.net.URL;
47-
import java.time.Instant;
48-
import java.time.temporal.ChronoUnit;
49-
import java.util.Collections;
5041
import java.util.Set;
5142

5243
/**
@@ -127,36 +118,22 @@ public Authentication authenticate(Authentication authentication) throws Authent
127118
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
128119
}
129120

130-
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
131-
132-
// TODO Allow configuration for issuer claim
133-
URL issuer = null;
134-
try {
135-
issuer = URI.create("https://oauth2.provider.com").toURL();
136-
} catch (MalformedURLException e) { }
137-
138-
Instant issuedAt = Instant.now();
139-
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); // TODO Allow configuration for access token time-to-live
140121
Set<String> authorizedScopes = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZED_SCOPES);
122+
Jwt jwt = OAuth2TokenIssuerUtil
123+
.issueJwtAccessToken(this.jwtEncoder, authorization.getPrincipalName(), registeredClient.getClientId(), authorizedScopes);
124+
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
125+
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), authorizedScopes);
141126

142-
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.withClaims()
143-
.issuer(issuer)
144-
.subject(authorization.getPrincipalName())
145-
.audience(Collections.singletonList(registeredClient.getClientId()))
146-
.issuedAt(issuedAt)
147-
.expiresAt(expiresAt)
148-
.notBefore(issuedAt)
149-
.claim(OAuth2ParameterNames.SCOPE, authorizedScopes)
150-
.build();
151-
152-
Jwt jwt = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
127+
OAuth2Tokens.Builder tokensBuilder = OAuth2Tokens.from(authorization.getTokens())
128+
.accessToken(accessToken);
153129

154-
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
155-
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
130+
OAuth2RefreshToken refreshToken = null;
131+
if (registeredClient.getTokenSettings().enableRefreshTokens()) {
132+
refreshToken = OAuth2TokenIssuerUtil.issueRefreshToken(registeredClient.getTokenSettings().refreshTokenTimeToLive());
133+
tokensBuilder.refreshToken(refreshToken);
134+
}
156135

157-
OAuth2Tokens tokens = OAuth2Tokens.from(authorization.getTokens())
158-
.accessToken(accessToken)
159-
.build();
136+
OAuth2Tokens tokens = tokensBuilder.build();
160137
tokens.invalidate(authorizationCode); // Invalidate the authorization code as it can only be used once
161138

162139
authorization = OAuth2Authorization.from(authorization)
@@ -165,7 +142,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
165142
.build();
166143
this.authorizationService.save(authorization);
167144

168-
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
145+
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken);
169146
}
170147

171148
@Override

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

+2-33
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@
2222
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2323
import org.springframework.security.oauth2.core.OAuth2Error;
2424
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
25-
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
26-
import org.springframework.security.oauth2.jose.JoseHeader;
27-
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
2825
import org.springframework.security.oauth2.jwt.Jwt;
29-
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
3026
import org.springframework.security.oauth2.jwt.JwtEncoder;
3127
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
3228
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
@@ -36,12 +32,6 @@
3632
import org.springframework.util.Assert;
3733
import org.springframework.util.CollectionUtils;
3834

39-
import java.net.MalformedURLException;
40-
import java.net.URI;
41-
import java.net.URL;
42-
import java.time.Instant;
43-
import java.time.temporal.ChronoUnit;
44-
import java.util.Collections;
4535
import java.util.LinkedHashSet;
4636
import java.util.Set;
4737
import java.util.stream.Collectors;
@@ -101,29 +91,8 @@ public Authentication authenticate(Authentication authentication) throws Authent
10191
scopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
10292
}
10393

104-
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
105-
106-
// TODO Allow configuration for issuer claim
107-
URL issuer = null;
108-
try {
109-
issuer = URI.create("https://oauth2.provider.com").toURL();
110-
} catch (MalformedURLException e) { }
111-
112-
Instant issuedAt = Instant.now();
113-
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); // TODO Allow configuration for access token time-to-live
114-
115-
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.withClaims()
116-
.issuer(issuer)
117-
.subject(clientPrincipal.getName())
118-
.audience(Collections.singletonList(registeredClient.getClientId()))
119-
.issuedAt(issuedAt)
120-
.expiresAt(expiresAt)
121-
.notBefore(issuedAt)
122-
.claim(OAuth2ParameterNames.SCOPE, scopes)
123-
.build();
124-
125-
Jwt jwt = this.jwtEncoder.encode(joseHeader, jwtClaimsSet);
126-
94+
Jwt jwt = OAuth2TokenIssuerUtil
95+
.issueJwtAccessToken(this.jwtEncoder, clientPrincipal.getName(), registeredClient.getClientId(), scopes);
12796
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
12897
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), scopes);
12998

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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.time.Instant;
20+
import java.util.Set;
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.oauth2.core.OAuth2AccessToken;
26+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
27+
import org.springframework.security.oauth2.core.OAuth2Error;
28+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
29+
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
30+
import org.springframework.security.oauth2.jwt.Jwt;
31+
import org.springframework.security.oauth2.jwt.JwtEncoder;
32+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
33+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
34+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
35+
import org.springframework.security.oauth2.server.authorization.TokenType;
36+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
37+
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
38+
import org.springframework.security.oauth2.server.authorization.token.OAuth2Tokens;
39+
import org.springframework.util.Assert;
40+
41+
/**
42+
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant.
43+
*
44+
* @author Alexey Nesterov
45+
* @since 0.0.3
46+
* @see OAuth2RefreshTokenAuthenticationToken
47+
* @see OAuth2AccessTokenAuthenticationToken
48+
* @see OAuth2AuthorizationService
49+
* @see JwtEncoder
50+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.5">Section 1.5 Refresh Token</a>
51+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
52+
*/
53+
54+
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
55+
56+
private final OAuth2AuthorizationService authorizationService;
57+
private final JwtEncoder jwtEncoder;
58+
59+
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) {
60+
Assert.notNull(authorizationService, "authorizationService cannot be null");
61+
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
62+
63+
this.authorizationService = authorizationService;
64+
this.jwtEncoder = jwtEncoder;
65+
}
66+
67+
@Override
68+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
69+
OAuth2RefreshTokenAuthenticationToken refreshTokenAuthentication =
70+
(OAuth2RefreshTokenAuthenticationToken) authentication;
71+
72+
OAuth2ClientAuthenticationToken clientPrincipal = null;
73+
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(refreshTokenAuthentication.getPrincipal().getClass())) {
74+
clientPrincipal = (OAuth2ClientAuthenticationToken) refreshTokenAuthentication.getPrincipal();
75+
}
76+
77+
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
78+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
79+
}
80+
81+
OAuth2Authorization authorization = this.authorizationService.findByToken(refreshTokenAuthentication.getRefreshToken(), TokenType.REFRESH_TOKEN);
82+
if (authorization == null) {
83+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
84+
}
85+
86+
Instant refreshTokenExpiration = authorization.getTokens().getRefreshToken().getExpiresAt();
87+
if (refreshTokenExpiration != null && refreshTokenExpiration.isBefore(Instant.now())) {
88+
// as per https://tools.ietf.org/html/rfc6749#section-5.2
89+
// invalid_grant: The provided authorization grant (e.g., authorization
90+
// code, resource owner credentials) or refresh token is invalid, expired, revoked [...].
91+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
92+
}
93+
94+
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
95+
96+
// https://tools.ietf.org/html/rfc6749#section-6
97+
// The requested scope MUST NOT include any scope not originally granted by the resource owner,
98+
// and if omitted is treated as equal to the scope originally granted by the resource owner.
99+
Set<String> refreshTokenScopes = refreshTokenAuthentication.getScopes();
100+
Set<String> authorizedScopes = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZED_SCOPES);
101+
if (!authorizedScopes.containsAll(refreshTokenScopes)) {
102+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
103+
}
104+
105+
if (refreshTokenScopes.isEmpty()) {
106+
refreshTokenScopes = authorizedScopes;
107+
}
108+
109+
Jwt jwt = OAuth2TokenIssuerUtil
110+
.issueJwtAccessToken(this.jwtEncoder, authorization.getPrincipalName(), registeredClient.getClientId(), refreshTokenScopes);
111+
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
112+
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), refreshTokenScopes);
113+
114+
TokenSettings tokenSettings = registeredClient.getTokenSettings();
115+
OAuth2RefreshToken refreshToken;
116+
if (tokenSettings.reuseRefreshTokens()) {
117+
refreshToken = authorization.getTokens().getRefreshToken();
118+
} else {
119+
refreshToken = OAuth2TokenIssuerUtil.issueRefreshToken(tokenSettings.refreshTokenTimeToLive());
120+
}
121+
122+
authorization = OAuth2Authorization.from(authorization)
123+
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, accessToken)
124+
.tokens(OAuth2Tokens.builder().accessToken(accessToken).refreshToken(refreshToken).build())
125+
.build();
126+
127+
this.authorizationService.save(authorization);
128+
129+
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken);
130+
}
131+
132+
@Override
133+
public boolean supports(Class<?> authentication) {
134+
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
135+
}
136+
}

0 commit comments

Comments
 (0)