Add Refresh Token grant type support

Closes gh-50
This commit is contained in:
Alexey Nesterov
2020-10-07 12:53:37 +03:00
committed by Joe Grandja
parent 1ce77d3caa
commit 78d4bd0bad
20 changed files with 1227 additions and 79 deletions

View File

@@ -217,6 +217,46 @@ public class OAuth2AuthorizationCodeGrantTests {
verify(authorizationService, times(2)).save(any());
}
@Test
public void requestWhenPublicClientWithRefreshThenReturnRefreshToken() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients
.registeredClient()
.clientSecret(null)
.tokenSettings(tokenSettings -> tokenSettings.enableRefreshTokens(true))
.build();
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient))
.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://example.com\\?code=.{15,}&state=state");
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization authorization = authorizationCaptor.getValue();
when(authorizationService.findByToken(
eq(authorization.getTokens().getToken(OAuth2AuthorizationCode.class).getTokenValue()),
eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorization))
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
.andExpect(status().isOk())
.andExpect(jsonPath("$.refresh_token").isNotEmpty());
}
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.time.Instant;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.crypto.keys.KeyManager;
import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.token.OAuth2Tokens;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Alexey Nesterov
* @since 0.0.3
*/
public class OAuth2RefreshTokenGrantTests {
private static final String TEST_REFRESH_TOKEN = "test-refresh-token";
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@Autowired
private MockMvc mvc;
private RegisteredClient registeredClient;
@BeforeClass
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
}
@Before
public void setup() {
reset(registeredClientRepository);
reset(authorizationService);
this.registeredClient = TestRegisteredClients.registeredClient2().build();
this.spring.register(OAuth2RefreshTokenGrantTests.AuthorizationServerConfiguration.class).autowire();
}
@Test
public void requestWhenRefreshTokenExists() throws Exception {
when(registeredClientRepository.findByClientId(eq(this.registeredClient.getClientId())))
.thenReturn(this.registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(this.registeredClient)
.tokens(OAuth2Tokens.builder()
.refreshToken(new OAuth2RefreshToken(TEST_REFRESH_TOKEN, Instant.now(), Instant.now().plusSeconds(60)))
.accessToken(new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(10)))
.build())
.build();
when(authorizationService.findByToken(TEST_REFRESH_TOKEN, TokenType.REFRESH_TOKEN))
.thenReturn(authorization);
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.getValue())
.param(OAuth2ParameterNames.REFRESH_TOKEN, TEST_REFRESH_TOKEN)
.with(httpBasic(this.registeredClient.getClientId(), this.registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty());
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
RegisteredClientRepository registeredClientRepository() {
return registeredClientRepository;
}
@Bean
OAuth2AuthorizationService authorizationService() {
return authorizationService;
}
@Bean
KeyManager keyManager() { return new StaticKeyGeneratingKeyManager(); }
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode;
@@ -141,6 +142,20 @@ public class InMemoryOAuth2AuthorizationServiceTests {
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenAndTokenTypeWhenTokenTypeRefreshTokenThenFound() {
final String refreshTokenValue = "refresh-token";
OAuth2Authorization expectedAuthorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.principalName(PRINCIPAL_NAME)
.tokens(OAuth2Tokens.builder().refreshToken(new OAuth2RefreshToken(refreshTokenValue, Instant.now().plusSeconds(10))).build())
.build();
this.authorizationService.save(expectedAuthorization);
OAuth2Authorization result = this.authorizationService.findByToken(
refreshTokenValue, TokenType.REFRESH_TOKEN);
assertThat(result).isEqualTo(expectedAuthorization);
}
@Test
public void findByTokenWhenTokenDoesNotExistThenNull() {
OAuth2Authorization result = this.authorizationService.findByToken(

View File

@@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode;
@@ -39,6 +40,7 @@ public class OAuth2AuthorizationTests {
private static final String PRINCIPAL_NAME = "principal";
private static final OAuth2AccessToken ACCESS_TOKEN = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300));
private static final OAuth2RefreshToken REFRESH_TOKEN = new OAuth2RefreshToken("refresh-token", Instant.now());
private static final OAuth2AuthorizationCode AUTHORIZATION_CODE = new OAuth2AuthorizationCode(
"code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES));
@@ -101,12 +103,13 @@ public class OAuth2AuthorizationTests {
public void buildWhenAllAttributesAreProvidedThenAllAttributesAreSet() {
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.principalName(PRINCIPAL_NAME)
.tokens(OAuth2Tokens.builder().token(AUTHORIZATION_CODE).accessToken(ACCESS_TOKEN).build())
.tokens(OAuth2Tokens.builder().token(AUTHORIZATION_CODE).accessToken(ACCESS_TOKEN).refreshToken(REFRESH_TOKEN).build())
.build();
assertThat(authorization.getRegisteredClientId()).isEqualTo(REGISTERED_CLIENT.getId());
assertThat(authorization.getPrincipalName()).isEqualTo(PRINCIPAL_NAME);
assertThat(authorization.getTokens().getToken(OAuth2AuthorizationCode.class)).isEqualTo(AUTHORIZATION_CODE);
assertThat(authorization.getTokens().getAccessToken()).isEqualTo(ACCESS_TOKEN);
assertThat(authorization.getTokens().getRefreshToken()).isEqualTo(REFRESH_TOKEN);
}
}

View File

@@ -15,9 +15,14 @@
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
@@ -37,12 +42,11 @@ import org.springframework.security.oauth2.server.authorization.client.InMemoryR
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenMetadata;
import org.springframework.security.oauth2.server.authorization.token.OAuth2Tokens;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@@ -60,6 +64,7 @@ import static org.mockito.Mockito.when;
* @author Daniel Garnier-Moiroux
*/
public class OAuth2AuthorizationCodeAuthenticationProviderTests {
private static final String AUTHORIZATION_CODE = "code";
private RegisteredClient registeredClient;
private RegisteredClientRepository registeredClientRepository;
@@ -230,6 +235,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
Set<String> scopes = jwtClaimsSet.getClaim(OAuth2ParameterNames.SCOPE);
assertThat(scopes).isEqualTo(authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZED_SCOPES));
assertThat(jwtClaimsSet.getSubject()).isEqualTo(authorization.getPrincipalName());
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
@@ -242,6 +248,87 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(updatedAuthorization.getTokens().getTokenMetadata(authorizationCode).isInvalidated()).isTrue();
}
@Test
public void authenticateWhenValidCodeThenReturnRefreshToken() {
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(this.registeredClient);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null);
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
assertThat(accessTokenAuthentication.getRefreshToken()).isNotNull();
assertThat(updatedAuthorization.getTokens().getRefreshToken()).isNotNull();
}
@Test
public void authenticateWhenTokenSettingsHasTimeToLiveThenRefreshTokenHasExpiration() {
Duration testRefreshTokenTTL = Duration.ofDays(1);
Duration defaultRefreshTokenTTL = new TokenSettings().refreshTokenTimeToLive();
RegisteredClient clientWithRefreshTokenTTLZero = TestRegisteredClients.registeredClient()
.tokenSettings(tokenSettings -> tokenSettings.refreshTokenTimeToLive(testRefreshTokenTTL))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(clientWithRefreshTokenTTLZero);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null);
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
assertThat(accessTokenAuthentication.getRefreshToken().getExpiresAt()).isAfter(Instant.now().plus(defaultRefreshTokenTTL));
assertThat(updatedAuthorization.getTokens().getRefreshToken().getExpiresAt()).isAfter(Instant.now().plus(defaultRefreshTokenTTL));
}
@Test
public void authenticateWhenRefreshTokenDisabledReturnNullRefreshCode() {
RegisteredClient clientWithRefreshTokenDisabled = TestRegisteredClients
.registeredClient()
.tokenSettings(tokenSettings -> tokenSettings.enableRefreshTokens(false))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(clientWithRefreshTokenDisabled);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null);
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
assertThat(accessTokenAuthentication.getRefreshToken()).isNull();
}
private static Jwt createJwt() {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);

View File

@@ -0,0 +1,288 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.jose.JoseHeaderNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.token.OAuth2Tokens;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Alexey Nesterov
* @since 0.0.3
*/
public class OAuth2RefreshTokenAuthenticationProviderTests {
private final String NEW_ACCESS_TOKEN_VALUE = UUID.randomUUID().toString();
private final String REFRESH_TOKEN_VALUE = UUID.randomUUID().toString();
private final RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
private final OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(this.registeredClient);
private final OAuth2AccessToken existingAccessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
"old-test-access-token",
Instant.now(),
Instant.now().plusSeconds(10),
this.registeredClient.getScopes());
private final OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(this.registeredClient)
.tokens(OAuth2Tokens.builder()
.accessToken(this.existingAccessToken)
.refreshToken(new OAuth2RefreshToken(REFRESH_TOKEN_VALUE, Instant.now(), Instant.now().plusSeconds(60)))
.build())
.build();
private OAuth2AuthorizationService authorizationService;
private JwtEncoder jwtEncoder;
private OAuth2RefreshTokenAuthenticationProvider provider;
@Before
public void setUp() {
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.jwtEncoder = mock(JwtEncoder.class);
this.provider = new OAuth2RefreshTokenAuthenticationProvider(this.authorizationService, this.jwtEncoder);
Jwt jwt = Jwt.withTokenValue(NEW_ACCESS_TOKEN_VALUE)
.issuedAt(Instant.now())
.header(JoseHeaderNames.ALG, SignatureAlgorithm.RS256.getName())
.build();
when(this.jwtEncoder.encode(any(), any())).thenReturn(jwt);
}
@Test
public void constructorWhenAuthorizationServiceNullThenThrowException() {
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationProvider(null, this.jwtEncoder))
.isInstanceOf(IllegalArgumentException.class)
.extracting(Throwable::getMessage)
.isEqualTo("authorizationService cannot be null");
}
@Test
public void constructorWhenJwtEncoderNullThenThrowException() {
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationProvider(this.authorizationService, null))
.isInstanceOf(IllegalArgumentException.class)
.extracting(Throwable::getMessage)
.isEqualTo("jwtEncoder cannot be null");
}
@Test
public void supportsWhenSupportedAuthenticationThenTrue() {
assertThat(this.provider.supports(OAuth2RefreshTokenAuthenticationToken.class)).isTrue();
}
@Test
public void supportsWhenUnsupportedAuthenticationThenFalse() {
assertThat(this.provider.supports(OAuth2ClientCredentialsAuthenticationToken.class)).isFalse();
}
@Test
public void authenticateWhenRefreshTokenExistsThenReturnAuthentication() {
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization);
OAuth2RefreshTokenAuthenticationToken token = new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, this.clientPrincipal);
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.provider.authenticate(token);
ArgumentCaptor<JwtClaimsSet> claimsSetArgumentCaptor = ArgumentCaptor.forClass(JwtClaimsSet.class);
verify(this.jwtEncoder).encode(any(), claimsSetArgumentCaptor.capture());
assertThat(claimsSetArgumentCaptor.getValue().getSubject()).isEqualTo(this.authorization.getPrincipalName());
assertThat(accessTokenAuthentication.getAccessToken()).isNotNull();
assertThat(accessTokenAuthentication.getAccessToken().getTokenValue()).isEqualTo(NEW_ACCESS_TOKEN_VALUE);
assertThat(accessTokenAuthentication.getAccessToken().getScopes()).containsAll(this.existingAccessToken.getScopes());
assertThat(accessTokenAuthentication.getPrincipal()).isEqualTo(this.clientPrincipal);
assertThat(accessTokenAuthentication.getRegisteredClient()).isEqualTo(this.registeredClient);
}
@Test
public void authenticateWhenRefreshTokenExistsThenUpdatesAuthorization() {
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization);
OAuth2RefreshTokenAuthenticationToken token = new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, this.clientPrincipal);
this.provider.authenticate(token);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
assertThat(updatedAuthorization.getTokens().getAccessToken()).isNotNull();
assertThat(updatedAuthorization.getTokens().getAccessToken().getTokenValue()).isEqualTo(NEW_ACCESS_TOKEN_VALUE);
}
@Test
public void authenticateWhenClientSetToReuseRefreshTokensThenKeepsRefreshTokenValue() {
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization);
RegisteredClient clientWithReuseTokensTrue = TestRegisteredClients.registeredClient()
.tokenSettings(tokenSettings -> tokenSettings.reuseRefreshTokens(true))
.build();
OAuth2RefreshTokenAuthenticationToken token = new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, new OAuth2ClientAuthenticationToken(clientWithReuseTokensTrue));
OAuth2AccessTokenAuthenticationToken authentication = (OAuth2AccessTokenAuthenticationToken) this.provider.authenticate(token);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
assertThat(updatedAuthorization.getTokens().getRefreshToken()).isNotNull();
assertThat(updatedAuthorization.getTokens().getRefreshToken()).isEqualTo(this.authorization.getTokens().getRefreshToken());
assertThat(authentication.getRefreshToken()).isEqualTo(this.authorization.getTokens().getRefreshToken());
}
@Test
public void authenticateWhenClientSetToGenerateNewRefreshTokensThenGenerateNewToken() {
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization);
RegisteredClient clientWithReuseTokensFalse = TestRegisteredClients.registeredClient()
.tokenSettings(tokenSettings -> tokenSettings.reuseRefreshTokens(false))
.build();
OAuth2RefreshTokenAuthenticationToken token =
new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, new OAuth2ClientAuthenticationToken(clientWithReuseTokensFalse));
OAuth2AccessTokenAuthenticationToken authentication = (OAuth2AccessTokenAuthenticationToken) this.provider.authenticate(token);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
assertThat(updatedAuthorization.getTokens().getRefreshToken()).isNotNull();
assertThat(updatedAuthorization.getTokens().getRefreshToken()).isNotEqualTo(this.authorization.getTokens().getRefreshToken());
assertThat(authentication.getRefreshToken()).isNotEqualTo(this.authorization.getTokens().getRefreshToken());
}
@Test
public void authenticateWhenRefreshTokenHasScopesThenIncludeScopes() {
Set<String> requestedScopes = new HashSet<>();
requestedScopes.add("email");
requestedScopes.add("openid");
OAuth2RefreshTokenAuthenticationToken tokenWithScopes
= new OAuth2RefreshTokenAuthenticationToken(this.clientPrincipal, REFRESH_TOKEN_VALUE, requestedScopes);
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization);
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.provider.authenticate(tokenWithScopes);
assertThat(accessTokenAuthentication.getAccessToken()).isNotNull();
assertThat(accessTokenAuthentication.getAccessToken().getScopes()).containsAll(requestedScopes);
}
@Test
public void authenticateWhenRefreshTokenHasNotApprovedScopesThenThrowException() {
Set<String> requestedScopes = new HashSet<>();
requestedScopes.add("email");
requestedScopes.add("another-scope");
OAuth2RefreshTokenAuthenticationToken tokenWithScopes
= new OAuth2RefreshTokenAuthenticationToken(this.clientPrincipal, REFRESH_TOKEN_VALUE, requestedScopes);
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization);
assertThatThrownBy(() -> this.provider.authenticate(tokenWithScopes))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting((Throwable e) -> ((OAuth2AuthenticationException) e).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE);
}
@Test
public void authenticateWhenRefreshTokenDoesNotExistThenThrowException() {
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(null);
OAuth2RefreshTokenAuthenticationToken token = new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, this.clientPrincipal);
assertThatThrownBy(() -> this.provider.authenticate(token))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(this.registeredClient.getClientId(), null);
OAuth2RefreshTokenAuthenticationToken token = new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, clientPrincipal);
Assertions.assertThatThrownBy(() -> this.provider.authenticate(token))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
}
@Test
public void authenticateWhenRefreshTokenHasExpiredThenThrowException() {
OAuth2RefreshToken expiredRefreshToken = new OAuth2RefreshToken(REFRESH_TOKEN_VALUE, Instant.now().minusSeconds(120), Instant.now().minusSeconds(60));
OAuth2Authorization authorizationWithExpiredRefreshToken =
OAuth2Authorization
.from(this.authorization)
.tokens(OAuth2Tokens.from(this.authorization.getTokens()).refreshToken(expiredRefreshToken).build())
.build();
OAuth2RefreshTokenAuthenticationToken token
= new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, this.clientPrincipal);
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(authorizationWithExpiredRefreshToken);
assertThatThrownBy(() -> this.provider.authenticate(token))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting((Throwable e) -> ((OAuth2AuthenticationException) e).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
/**
* @author Alexey Nesterov
* @since 0.0.3
*/
public class OAuth2RefreshTokenAuthenticationTokenTests {
@Test
public void constructorWhenClientPrincipalNullThrowException() {
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("", null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("clientPrincipal cannot be null");
}
@Test
public void constructorWhenRefreshTokenNullOrEmptyThrowException() {
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken(null, mock(OAuth2ClientAuthenticationToken.class)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("refreshToken cannot be null or empty");
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("", mock(OAuth2ClientAuthenticationToken.class)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("refreshToken cannot be null or empty");
}
@Test
public void constructorWhenGettingScopesThenReturnRequestedScopes() {
Set<String> expectedScopes = new HashSet<>(Arrays.asList("scope-a", "scope-b"));
OAuth2RefreshTokenAuthenticationToken token
= new OAuth2RefreshTokenAuthenticationToken(mock(OAuth2ClientAuthenticationToken.class), "test", expectedScopes);
assertThat(token.getScopes()).containsAll(expectedScopes);
}
}

View File

@@ -32,8 +32,11 @@ public class TokenSettingsTests {
@Test
public void constructorWhenDefaultThenDefaultsAreSet() {
TokenSettings tokenSettings = new TokenSettings();
assertThat(tokenSettings.settings()).hasSize(1);
assertThat(tokenSettings.settings()).hasSize(4);
assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
assertThat(tokenSettings.enableRefreshTokens()).isTrue();
assertThat(tokenSettings.reuseRefreshTokens()).isEqualTo(false);
assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
}
@Test
@@ -50,6 +53,44 @@ public class TokenSettingsTests {
assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(accessTokenTimeToLive);
}
@Test
public void enableRefreshTokenWhenFalseThenSet() {
TokenSettings tokenSettings = new TokenSettings().enableRefreshTokens(false);
assertThat(tokenSettings.enableRefreshTokens()).isFalse();
}
@Test
public void reuseRefreshTokensWhenProvidedThenSet() {
boolean reuseRefreshTokens = true;
TokenSettings tokenSettings = new TokenSettings().reuseRefreshTokens(reuseRefreshTokens);
assertThat(tokenSettings.reuseRefreshTokens()).isEqualTo(reuseRefreshTokens);
}
@Test
public void refreshTokenTimeToLiveWhenProvidedThenSet() {
Duration refresTokenTimeToLive = Duration.ofDays(10);
TokenSettings tokenSettings = new TokenSettings().refreshTokenTimeToLive(refresTokenTimeToLive);
assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(refresTokenTimeToLive);
}
@Test
public void refreshTokenTimeToLiveWhenZeroOrNegativeThenThrowException() {
assertThatThrownBy(() -> new TokenSettings().refreshTokenTimeToLive(null))
.isInstanceOf(IllegalArgumentException.class)
.extracting(Throwable::getMessage)
.isEqualTo("refreshTokenTimeToLive cannot be null");
assertThatThrownBy(() -> new TokenSettings().refreshTokenTimeToLive(Duration.ZERO))
.isInstanceOf(IllegalArgumentException.class)
.extracting(Throwable::getMessage)
.isEqualTo("refreshTokenTimeToLive has to be greater than Duration.ZERO");
assertThatThrownBy(() -> new TokenSettings().refreshTokenTimeToLive(Duration.ofSeconds(-10)))
.isInstanceOf(IllegalArgumentException.class)
.extracting(Throwable::getMessage)
.isEqualTo("refreshTokenTimeToLive has to be greater than Duration.ZERO");
}
@Test
public void settingWhenCalledThenReturnTokenSettings() {
Duration accessTokenTimeToLive = Duration.ofMinutes(10);
@@ -57,8 +98,11 @@ public class TokenSettingsTests {
.<TokenSettings>setting("name1", "value1")
.accessTokenTimeToLive(accessTokenTimeToLive)
.<TokenSettings>settings(settings -> settings.put("name2", "value2"));
assertThat(tokenSettings.settings()).hasSize(3);
assertThat(tokenSettings.settings()).hasSize(6);
assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(accessTokenTimeToLive);
assertThat(tokenSettings.enableRefreshTokens()).isTrue();
assertThat(tokenSettings.reuseRefreshTokens()).isFalse();
assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
assertThat(tokenSettings.<String>setting("name1")).isEqualTo("value1");
assertThat(tokenSettings.<String>setting("name2")).isEqualTo("value2");
}

View File

@@ -32,6 +32,7 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
@@ -41,6 +42,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.util.StringUtils;
@@ -311,6 +313,56 @@ public class OAuth2TokenEndpointFilterTests {
assertThat(accessTokenResult.getScopes()).isEqualTo(accessToken.getScopes());
}
@Test
public void doFilterWhenRefreshTokenRequestValidThenAccessTokenResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "token",
Instant.now(), Instant.now().plus(Duration.ofHours(1)),
new HashSet<>(Arrays.asList("scope1", "scope2")));
String refreshTokenValue = "refresh-token";
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(refreshTokenValue, Instant.now());
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
new OAuth2AccessTokenAuthenticationToken(
registeredClient, clientPrincipal, accessToken, refreshToken);
when(this.authenticationManager.authenticate(any())).thenReturn(accessTokenAuthentication);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(clientPrincipal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = createRefreshTokenTokenRequest(registeredClient, refreshTokenValue, null);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
ArgumentCaptor<OAuth2RefreshTokenAuthenticationToken> argumentCaptor =
ArgumentCaptor.forClass(OAuth2RefreshTokenAuthenticationToken.class);
verify(this.authenticationManager).authenticate(argumentCaptor.capture());
OAuth2RefreshTokenAuthenticationToken refreshTokenAuthenticationToken =
argumentCaptor.getValue();
assertThat(refreshTokenAuthenticationToken.getPrincipal()).isEqualTo(clientPrincipal);
assertThat(refreshTokenAuthenticationToken.getScopes()).isEqualTo(registeredClient.getScopes());
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
OAuth2AccessTokenResponse accessTokenResponse = readAccessTokenResponse(response);
OAuth2AccessToken accessTokenResult = accessTokenResponse.getAccessToken();
assertThat(accessTokenResult.getTokenType()).isEqualTo(accessToken.getTokenType());
assertThat(accessTokenResult.getTokenValue()).isEqualTo(accessToken.getTokenValue());
assertThat(accessTokenResult.getIssuedAt()).isBetween(
accessToken.getIssuedAt().minusSeconds(1), accessToken.getIssuedAt().plusSeconds(1));
assertThat(accessTokenResult.getExpiresAt()).isBetween(
accessToken.getExpiresAt().minusSeconds(1), accessToken.getExpiresAt().plusSeconds(1));
assertThat(accessTokenResult.getScopes()).isEqualTo(accessToken.getScopes());
}
private void doFilterWhenTokenRequestInvalidParameterThenError(String parameterName, String errorCode,
MockHttpServletRequest request) throws Exception {
@@ -366,4 +418,21 @@ public class OAuth2TokenEndpointFilterTests {
return request;
}
private static MockHttpServletRequest createRefreshTokenTokenRequest(RegisteredClient registeredClient, String refreshToken, String scope) {
String requestUri = OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.getValue());
request.addParameter(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken);
if (scope == null) {
request.addParameter(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
} else {
request.addParameter(OAuth2ParameterNames.SCOPE, scope);
}
return request;
}
}