From 3ea7d8c9b6319dad08fbb19787752ff546a8b0fa Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 28 Jul 2021 05:15:32 -0400 Subject: [PATCH] Provide configuration for refresh token generator Closes gh-377 --- ...thorizationCodeAuthenticationProvider.java | 28 ++++++++++++- ...th2RefreshTokenAuthenticationProvider.java | 19 +++++++-- ...zationCodeAuthenticationProviderTests.java | 40 +++++++++++++++++++ ...freshTokenAuthenticationProviderTests.java | 40 +++++++++++++++++++ 4 files changed, 122 insertions(+), 5 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index 6e0e283b..c3e9b72a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -16,15 +16,21 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.security.Principal; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -74,9 +80,12 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth new OAuth2TokenType(OAuth2ParameterNames.CODE); private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN); + private static final StringKeyGenerator DEFAULT_REFRESH_TOKEN_GENERATOR = + new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); private final OAuth2AuthorizationService authorizationService; private final JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer = (context) -> {}; + private Supplier refreshTokenGenerator = DEFAULT_REFRESH_TOKEN_GENERATOR::generateKey; private ProviderSettings providerSettings; /** @@ -97,6 +106,16 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth this.jwtCustomizer = jwtCustomizer; } + /** + * Sets the {@code Supplier} that generates the value for the {@link OAuth2RefreshToken}. + * + * @param refreshTokenGenerator the {@code Supplier} that generates the value for the {@link OAuth2RefreshToken} + */ + public void setRefreshTokenGenerator(Supplier refreshTokenGenerator) { + Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null"); + this.refreshTokenGenerator = refreshTokenGenerator; + } + @Autowired(required = false) protected void setProviderSettings(ProviderSettings providerSettings) { this.providerSettings = providerSettings; @@ -173,8 +192,7 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth OAuth2RefreshToken refreshToken = null; if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) { - refreshToken = OAuth2RefreshTokenAuthenticationProvider.generateRefreshToken( - registeredClient.getTokenSettings().getRefreshTokenTimeToLive()); + refreshToken = generateRefreshToken(registeredClient.getTokenSettings().getRefreshTokenTimeToLive()); } Jwt jwtIdToken = null; @@ -250,4 +268,10 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication); } + private OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) { + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(tokenTimeToLive); + return new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt); + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java index 18ed2ebe..0f7c3786 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; @@ -73,10 +74,12 @@ import static org.springframework.security.oauth2.server.authorization.authentic */ public final class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider { private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN); - private static final StringKeyGenerator TOKEN_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + private static final StringKeyGenerator DEFAULT_REFRESH_TOKEN_GENERATOR = + new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); private final OAuth2AuthorizationService authorizationService; private final JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer = (context) -> {}; + private Supplier refreshTokenGenerator = DEFAULT_REFRESH_TOKEN_GENERATOR::generateKey; private ProviderSettings providerSettings; /** @@ -98,6 +101,16 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic this.jwtCustomizer = jwtCustomizer; } + /** + * Sets the {@code Supplier} that generates the value for the {@link OAuth2RefreshToken}. + * + * @param refreshTokenGenerator the {@code Supplier} that generates the value for the {@link OAuth2RefreshToken} + */ + public void setRefreshTokenGenerator(Supplier refreshTokenGenerator) { + Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null"); + this.refreshTokenGenerator = refreshTokenGenerator; + } + @Autowired(required = false) protected void setProviderSettings(ProviderSettings providerSettings) { this.providerSettings = providerSettings; @@ -246,9 +259,9 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication); } - static OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) { + private OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(tokenTimeToLive); - return new OAuth2RefreshToken(TOKEN_GENERATOR.generateKey(), issuedAt, expiresAt); + return new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index 40ecbbc9..490de633 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.junit.Before; import org.junit.Test; @@ -61,6 +62,7 @@ import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -110,6 +112,13 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { .hasMessage("jwtCustomizer cannot be null"); } + @Test + public void setRefreshTokenGeneratorWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authenticationProvider.setRefreshTokenGenerator(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("refreshTokenGenerator cannot be null"); + } + @Test public void supportsWhenTypeOAuth2AuthorizationCodeAuthenticationTokenThenReturnTrue() { assertThat(this.authenticationProvider.supports(OAuth2AuthorizationCodeAuthenticationToken.class)).isTrue(); @@ -440,6 +449,37 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { assertThat(accessTokenAuthentication.getRefreshToken()).isNull(); } + @Test + public void authenticateWhenCustomRefreshTokenGeneratorThenUsed() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(AUTHORIZATION_CODE_TOKEN_TYPE))) + .thenReturn(authorization); + + when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt()); + + @SuppressWarnings("unchecked") + Supplier refreshTokenGenerator = spy(new Supplier() { + @Override + public String get() { + return "custom-refresh-token"; + } + }); + this.authenticationProvider.setRefreshTokenGenerator(refreshTokenGenerator); + + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( + OAuth2AuthorizationRequest.class.getName()); + OAuth2AuthorizationCodeAuthenticationToken authentication = + new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null); + + OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = + (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verify(refreshTokenGenerator).get(); + assertThat(accessTokenAuthentication.getRefreshToken().getTokenValue()).isEqualTo(refreshTokenGenerator.get()); + } + private static Jwt createJwt() { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java index bd35105a..df452b4e 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.junit.Before; import org.junit.Test; @@ -59,6 +60,7 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -111,6 +113,13 @@ public class OAuth2RefreshTokenAuthenticationProviderTests { .hasMessage("jwtCustomizer cannot be null"); } + @Test + public void setRefreshTokenGeneratorWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authenticationProvider.setRefreshTokenGenerator(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("refreshTokenGenerator cannot be null"); + } + @Test public void supportsWhenSupportedAuthenticationThenTrue() { assertThat(this.authenticationProvider.supports(OAuth2RefreshTokenAuthenticationToken.class)).isTrue(); @@ -281,6 +290,37 @@ public class OAuth2RefreshTokenAuthenticationProviderTests { assertThat(accessTokenAuthentication.getAccessToken().getScopes()).isEqualTo(requestedScopes); } + @Test + public void authenticateWhenCustomRefreshTokenGeneratorThenUsed() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .tokenSettings(TokenSettings.builder().reuseRefreshTokens(false).build()) + .build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + when(this.authorizationService.findByToken( + eq(authorization.getRefreshToken().getToken().getTokenValue()), + eq(OAuth2TokenType.REFRESH_TOKEN))) + .thenReturn(authorization); + + @SuppressWarnings("unchecked") + Supplier refreshTokenGenerator = spy(new Supplier() { + @Override + public String get() { + return "custom-refresh-token"; + } + }); + this.authenticationProvider.setRefreshTokenGenerator(refreshTokenGenerator); + + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); + OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken( + authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null, null); + + OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = + (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verify(refreshTokenGenerator).get(); + assertThat(accessTokenAuthentication.getRefreshToken().getTokenValue()).isEqualTo(refreshTokenGenerator.get()); + } + @Test public void authenticateWhenRequestedScopesNotAuthorizedThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();