Skip to content

Commit 9863a01

Browse files
committedJul 1, 2021
Implement refresh token rotated feature for public clients spring-projectsgh-297
1 parent a949998 commit 9863a01

File tree

2 files changed

+116
-40
lines changed

2 files changed

+116
-40
lines changed
 

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

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

18-
import java.security.Principal;
19-
import java.time.Duration;
20-
import java.time.Instant;
21-
import java.util.Base64;
22-
import java.util.Set;
23-
2418
import org.springframework.beans.factory.annotation.Autowired;
2519
import org.springframework.security.authentication.AuthenticationProvider;
2620
import org.springframework.security.core.Authentication;
2721
import org.springframework.security.core.AuthenticationException;
2822
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
2923
import org.springframework.security.crypto.keygen.StringKeyGenerator;
30-
import org.springframework.security.oauth2.core.AuthorizationGrantType;
31-
import org.springframework.security.oauth2.core.OAuth2AccessToken;
32-
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
33-
import org.springframework.security.oauth2.core.OAuth2Error;
34-
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
35-
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
36-
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
37-
import org.springframework.security.oauth2.core.OAuth2TokenType;
24+
import org.springframework.security.oauth2.core.*;
3825
import org.springframework.security.oauth2.jwt.JoseHeader;
3926
import org.springframework.security.oauth2.jwt.Jwt;
4027
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
4128
import org.springframework.security.oauth2.jwt.JwtEncoder;
29+
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
4230
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
4331
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
32+
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
4433
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
4534
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
4635
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
47-
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
48-
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
4936
import org.springframework.util.Assert;
37+
import org.springframework.util.StringUtils;
38+
39+
import java.security.Principal;
40+
import java.time.Duration;
41+
import java.time.Instant;
42+
import java.util.Base64;
43+
import java.util.Set;
5044

5145
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
5246

@@ -64,6 +58,7 @@
6458
* @see JwtEncodingContext
6559
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.5">Section 1.5 Refresh Token Grant</a>
6660
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
61+
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps-07#section-8">Section 8 Refresh Tokens</a>
6762
*/
6863
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
6964
private static final StringKeyGenerator TOKEN_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
@@ -171,7 +166,20 @@ public Authentication authenticate(Authentication authentication) throws Authent
171166

172167
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
173168
if (!tokenSettings.reuseRefreshTokens()) {
174-
currentRefreshToken = generateRefreshToken(tokenSettings.refreshTokenTimeToLive());
169+
Duration refreshTokenTimeToLive = tokenSettings.refreshTokenTimeToLive();
170+
boolean isPublicClient = !StringUtils.hasText(registeredClient.getClientSecret());
171+
if (isPublicClient) {
172+
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps-07#section-8
173+
// - SHOULD rotate refresh tokens on each use, in order to be able to
174+
// detect a stolen refresh token if one is replayed
175+
// - upon issuing a rotated refresh token, MUST NOT extend the lifetime
176+
// of the new refresh token beyond the lifetime of the initial
177+
// refresh token if the refresh token has a preestablished expiration time
178+
currentRefreshToken = generateReducedRefreshToken(refreshTokenTimeToLive,
179+
currentRefreshToken.getIssuedAt());
180+
} else {
181+
currentRefreshToken = generateRefreshToken(refreshTokenTimeToLive);
182+
}
175183
}
176184

177185
// @formatter:off
@@ -199,4 +207,17 @@ static OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) {
199207
Instant expiresAt = issuedAt.plus(tokenTimeToLive);
200208
return new OAuth2RefreshToken2(TOKEN_GENERATOR.generateKey(), issuedAt, expiresAt);
201209
}
210+
211+
private static OAuth2RefreshToken generateReducedRefreshToken(Duration tokenTimeToLive,
212+
Instant currentRefreshTokenIssuedAt) {
213+
Duration reducedTimeToLife;
214+
if (currentRefreshTokenIssuedAt != null) {
215+
Duration currentTokenDisuseDuration = Duration.between(currentRefreshTokenIssuedAt, Instant.now());
216+
reducedTimeToLife = tokenTimeToLive.minus(currentTokenDisuseDuration);
217+
} else {
218+
reducedTimeToLife = tokenTimeToLive;
219+
}
220+
221+
return generateRefreshToken(reducedTimeToLife);
222+
}
202223
}

‎oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java

+78-23
Original file line numberDiff line numberDiff line change
@@ -15,46 +15,34 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.authentication;
1717

18-
import java.security.Principal;
19-
import java.time.Instant;
20-
import java.time.temporal.ChronoUnit;
21-
import java.util.Collections;
22-
import java.util.HashSet;
23-
import java.util.Set;
24-
2518
import org.junit.Before;
2619
import org.junit.Test;
2720
import org.mockito.ArgumentCaptor;
28-
2921
import org.springframework.security.authentication.TestingAuthenticationToken;
3022
import org.springframework.security.core.Authentication;
31-
import org.springframework.security.oauth2.core.AuthorizationGrantType;
32-
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
33-
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
34-
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
35-
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
36-
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
37-
import org.springframework.security.oauth2.core.OAuth2TokenType;
23+
import org.springframework.security.oauth2.core.*;
3824
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
3925
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
4026
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
4127
import org.springframework.security.oauth2.jwt.Jwt;
4228
import org.springframework.security.oauth2.jwt.JwtEncoder;
43-
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
44-
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
45-
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
29+
import org.springframework.security.oauth2.server.authorization.*;
4630
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
4731
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
48-
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
49-
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
32+
33+
import java.security.Principal;
34+
import java.time.Duration;
35+
import java.time.Instant;
36+
import java.time.temporal.ChronoUnit;
37+
import java.util.Collections;
38+
import java.util.HashSet;
39+
import java.util.Set;
5040

5141
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
5242
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
5343
import static org.mockito.ArgumentMatchers.any;
5444
import static org.mockito.ArgumentMatchers.eq;
55-
import static org.mockito.Mockito.mock;
56-
import static org.mockito.Mockito.verify;
57-
import static org.mockito.Mockito.when;
45+
import static org.mockito.Mockito.*;
5846

5947
/**
6048
* Tests for {@link OAuth2RefreshTokenAuthenticationProvider}.
@@ -364,6 +352,73 @@ public void authenticateWhenRevokedRefreshTokenThenThrowOAuth2AuthenticationExce
364352
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
365353
}
366354

355+
@Test
356+
public void authenticateWhenClientIsPublicThenIssueReducedRefreshToken() {
357+
Duration refreshTokenTimeToLive = Duration.ofHours(24);
358+
Duration currentTokenDisuseDuration = Duration.ofHours(1);
359+
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient()
360+
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
361+
.tokenSettings(tokenSettings -> {
362+
tokenSettings.reuseRefreshTokens(false);
363+
tokenSettings.refreshTokenTimeToLive(refreshTokenTimeToLive);
364+
})
365+
.build();
366+
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2("refresh-token",
367+
Instant.now().minus(currentTokenDisuseDuration), Instant.now().plus(refreshTokenTimeToLive));
368+
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
369+
.token(refreshToken)
370+
.build();
371+
when(this.authorizationService.findByToken(
372+
eq(authorization.getRefreshToken().getToken().getTokenValue()),
373+
eq(OAuth2TokenType.REFRESH_TOKEN)))
374+
.thenReturn(authorization);
375+
376+
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
377+
OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken(
378+
authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null, null);
379+
380+
OAuth2AccessTokenAuthenticationToken authenticationToken = (OAuth2AccessTokenAuthenticationToken)
381+
this.authenticationProvider.authenticate(authentication);
382+
383+
assertThat(authenticationToken.getRefreshToken()).isNotNull();
384+
assertThat(authenticationToken.getRefreshToken().getExpiresAt())
385+
.isNotNull()
386+
.isBeforeOrEqualTo(Instant.now().plus(refreshTokenTimeToLive.minus(currentTokenDisuseDuration)));
387+
}
388+
389+
@Test
390+
public void authenticateWhenClientIsPublicAndCurrentTokenHasNotIssuedAtThenGenerateRefreshToken() {
391+
Duration refreshTokenTimeToLive = Duration.ofHours(24);
392+
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient()
393+
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
394+
.tokenSettings(tokenSettings -> {
395+
tokenSettings.reuseRefreshTokens(false);
396+
tokenSettings.refreshTokenTimeToLive(refreshTokenTimeToLive);
397+
})
398+
.build();
399+
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2("refresh-token",
400+
null, Instant.now().plus(refreshTokenTimeToLive));
401+
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
402+
.token(refreshToken)
403+
.build();
404+
when(this.authorizationService.findByToken(
405+
eq(authorization.getRefreshToken().getToken().getTokenValue()),
406+
eq(OAuth2TokenType.REFRESH_TOKEN)))
407+
.thenReturn(authorization);
408+
409+
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
410+
OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken(
411+
authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null, null);
412+
413+
OAuth2AccessTokenAuthenticationToken authenticationToken = (OAuth2AccessTokenAuthenticationToken)
414+
this.authenticationProvider.authenticate(authentication);
415+
416+
assertThat(authenticationToken.getRefreshToken()).isNotNull();
417+
assertThat(authenticationToken.getRefreshToken().getExpiresAt())
418+
.isNotNull()
419+
.isBeforeOrEqualTo(Instant.now().plus(refreshTokenTimeToLive));
420+
}
421+
367422
private static Jwt createJwt(Set<String> scope) {
368423
Instant issuedAt = Instant.now();
369424
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);

0 commit comments

Comments
 (0)