20 changed files with 1226 additions and 78 deletions
@ -0,0 +1,136 @@
@@ -0,0 +1,136 @@
|
||||
/* |
||||
* 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.Set; |
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken; |
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; |
||||
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.jwt.Jwt; |
||||
import org.springframework.security.oauth2.jwt.JwtEncoder; |
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; |
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames; |
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; |
||||
import org.springframework.security.oauth2.server.authorization.TokenType; |
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; |
||||
import org.springframework.security.oauth2.server.authorization.config.TokenSettings; |
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2Tokens; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant. |
||||
* |
||||
* @author Alexey Nesterov |
||||
* @since 0.0.3 |
||||
* @see OAuth2RefreshTokenAuthenticationToken |
||||
* @see OAuth2AccessTokenAuthenticationToken |
||||
* @see OAuth2AuthorizationService |
||||
* @see JwtEncoder |
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.5">Section 1.5 Refresh Token</a> |
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a> |
||||
*/ |
||||
|
||||
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider { |
||||
|
||||
private final OAuth2AuthorizationService authorizationService; |
||||
private final JwtEncoder jwtEncoder; |
||||
|
||||
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) { |
||||
Assert.notNull(authorizationService, "authorizationService cannot be null"); |
||||
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null"); |
||||
|
||||
this.authorizationService = authorizationService; |
||||
this.jwtEncoder = jwtEncoder; |
||||
} |
||||
|
||||
@Override |
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
||||
OAuth2RefreshTokenAuthenticationToken refreshTokenAuthentication = |
||||
(OAuth2RefreshTokenAuthenticationToken) authentication; |
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null; |
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(refreshTokenAuthentication.getPrincipal().getClass())) { |
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) refreshTokenAuthentication.getPrincipal(); |
||||
} |
||||
|
||||
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) { |
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)); |
||||
} |
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(refreshTokenAuthentication.getRefreshToken(), TokenType.REFRESH_TOKEN); |
||||
if (authorization == null) { |
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); |
||||
} |
||||
|
||||
Instant refreshTokenExpiration = authorization.getTokens().getRefreshToken().getExpiresAt(); |
||||
if (refreshTokenExpiration.isBefore(Instant.now())) { |
||||
// as per https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
// invalid_grant: The provided authorization grant (e.g., authorization
|
||||
// code, resource owner credentials) or refresh token is invalid, expired, revoked [...].
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); |
||||
} |
||||
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); |
||||
|
||||
// https://tools.ietf.org/html/rfc6749#section-6
|
||||
// The requested scope MUST NOT include any scope not originally granted by the resource owner,
|
||||
// and if omitted is treated as equal to the scope originally granted by the resource owner.
|
||||
Set<String> refreshTokenScopes = refreshTokenAuthentication.getScopes(); |
||||
Set<String> authorizedScopes = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZED_SCOPES); |
||||
if (!authorizedScopes.containsAll(refreshTokenScopes)) { |
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE)); |
||||
} |
||||
|
||||
if (refreshTokenScopes.isEmpty()) { |
||||
refreshTokenScopes = authorizedScopes; |
||||
} |
||||
|
||||
Jwt jwt = OAuth2TokenIssuerUtil |
||||
.issueJwtAccessToken(this.jwtEncoder, authorization.getPrincipalName(), registeredClient.getClientId(), refreshTokenScopes); |
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, |
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), refreshTokenScopes); |
||||
|
||||
TokenSettings tokenSettings = registeredClient.getTokenSettings(); |
||||
OAuth2RefreshToken refreshToken; |
||||
if (tokenSettings.reuseRefreshTokens()) { |
||||
refreshToken = authorization.getTokens().getRefreshToken(); |
||||
} else { |
||||
refreshToken = OAuth2TokenIssuerUtil.issueRefreshToken(tokenSettings.refreshTokenTimeToLive()); |
||||
} |
||||
|
||||
authorization = OAuth2Authorization.from(authorization) |
||||
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt) |
||||
.tokens(OAuth2Tokens.builder().accessToken(accessToken).refreshToken(refreshToken).build()) |
||||
.build(); |
||||
|
||||
this.authorizationService.save(authorization); |
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken); |
||||
} |
||||
|
||||
@Override |
||||
public boolean supports(Class<?> authentication) { |
||||
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication); |
||||
} |
||||
} |
||||
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
/* |
||||
* 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.Collections; |
||||
import java.util.Set; |
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An {@link Authentication} implementation used for the OAuth 2.0 Refresh Token Grant. |
||||
* |
||||
* @author Alexey Nesterov |
||||
* @since 0.0.3 |
||||
* @see AbstractAuthenticationToken |
||||
* @see OAuth2RefreshTokenAuthenticationProvider |
||||
* @see OAuth2ClientAuthenticationToken |
||||
*/ |
||||
public class OAuth2RefreshTokenAuthenticationToken extends AbstractAuthenticationToken { |
||||
|
||||
private final Authentication clientPrincipal; |
||||
private final String refreshToken; |
||||
private final Set<String> scopes; |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters. |
||||
* |
||||
* @param refreshToken refresh token value |
||||
* @param clientPrincipal the authenticated client principal |
||||
*/ |
||||
public OAuth2RefreshTokenAuthenticationToken(String refreshToken, Authentication clientPrincipal) { |
||||
this(clientPrincipal, refreshToken, Collections.emptySet()); |
||||
} |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters. |
||||
* |
||||
* @param clientPrincipal the authenticated client principal |
||||
* @param refreshToken refresh token value |
||||
* @param requestedScopes scopes requested by refresh token |
||||
*/ |
||||
public OAuth2RefreshTokenAuthenticationToken(Authentication clientPrincipal, String refreshToken, Set<String> requestedScopes) { |
||||
super(Collections.emptySet()); |
||||
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); |
||||
Assert.hasText(refreshToken, "refreshToken cannot be null or empty"); |
||||
|
||||
this.clientPrincipal = clientPrincipal; |
||||
this.refreshToken = refreshToken; |
||||
this.scopes = requestedScopes; |
||||
} |
||||
|
||||
@Override |
||||
public Object getCredentials() { |
||||
return ""; |
||||
} |
||||
|
||||
@Override |
||||
public Object getPrincipal() { |
||||
return this.clientPrincipal; |
||||
} |
||||
|
||||
public String getRefreshToken() { |
||||
return this.refreshToken; |
||||
} |
||||
|
||||
/** |
||||
* Returns the requested scope(s). |
||||
* |
||||
* @return the requested scope(s), or an empty {@code Set} if not available |
||||
*/ |
||||
public Set<String> getScopes() { |
||||
return this.scopes; |
||||
} |
||||
} |
||||
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
/* |
||||
* 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.net.MalformedURLException; |
||||
import java.net.URI; |
||||
import java.net.URL; |
||||
import java.time.Duration; |
||||
import java.time.Instant; |
||||
import java.time.temporal.ChronoUnit; |
||||
import java.util.Base64; |
||||
import java.util.Collections; |
||||
import java.util.Set; |
||||
|
||||
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; |
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator; |
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
||||
import org.springframework.security.oauth2.jose.JoseHeader; |
||||
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; |
||||
|
||||
/** |
||||
* @author Alexey Nesterov |
||||
* @since 0.0.3 |
||||
*/ |
||||
class OAuth2TokenIssuerUtil { |
||||
|
||||
private static final StringKeyGenerator CODE_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); |
||||
|
||||
static Jwt issueJwtAccessToken(JwtEncoder jwtEncoder, String subject, String audience, Set<String> scopes) { |
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build(); |
||||
|
||||
// TODO Allow configuration for issuer claim
|
||||
URL issuer = null; |
||||
try { |
||||
issuer = URI.create("https://oauth2.provider.com").toURL(); |
||||
} catch (MalformedURLException e) { } |
||||
|
||||
Instant issuedAt = Instant.now(); |
||||
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); // TODO Allow configuration for access token time-to-live
|
||||
|
||||
JwtClaimsSet jwtClaimsSet = JwtClaimsSet.withClaims() |
||||
.issuer(issuer) |
||||
.subject(subject) |
||||
.audience(Collections.singletonList(audience)) |
||||
.issuedAt(issuedAt) |
||||
.expiresAt(expiresAt) |
||||
.notBefore(issuedAt) |
||||
.claim(OAuth2ParameterNames.SCOPE, scopes) |
||||
.build(); |
||||
|
||||
return jwtEncoder.encode(joseHeader, jwtClaimsSet); |
||||
} |
||||
|
||||
static OAuth2RefreshToken issueRefreshToken(Duration refreshTokenTimeToLive) { |
||||
Instant issuedAt = Instant.now(); |
||||
Instant refreshTokenExpiresAt = issuedAt.plus(refreshTokenTimeToLive); |
||||
|
||||
return new OAuth2RefreshToken(CODE_GENERATOR.generateKey(), issuedAt, refreshTokenExpiresAt); |
||||
} |
||||
} |
||||
@ -0,0 +1,133 @@
@@ -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(); } |
||||
} |
||||
} |
||||
@ -0,0 +1,288 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,61 @@
@@ -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); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue