Implement Proof Key for Code Exchange (PKCE) RFC 7636

See https://tools.ietf.org/html/rfc7636

Closes gh-45
This commit is contained in:
Daniel Garnier-Moiroux
2020-06-22 21:35:01 +02:00
committed by Joe Grandja
parent 8541f6be69
commit ab090445b3
13 changed files with 1008 additions and 55 deletions

View File

@@ -31,6 +31,7 @@ import org.springframework.security.crypto.keys.StaticKeyGeneratingKeyManager;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -169,6 +170,7 @@ public class OAuth2AuthorizationCodeGrantTests {
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
parameters.set(OAuth2ParameterNames.STATE, "state");
parameters.set(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
return parameters;
}

View File

@@ -0,0 +1,178 @@
/*
* 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 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.security.config.annotation.web.WebSecurityConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
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.ClientSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import static org.assertj.core.api.Assertions.assertThat;
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.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
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;
public class OAuth2PkceTests {
private static RegisteredClientRepository registeredClientRepository;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@Autowired
private MockMvc mvc;
@BeforeClass
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
}
@Before
public void setup() {
reset(registeredClientRepository);
}
@Test
public void requestWhenTokenRequestNotAuthenticatedAndPkceParamatersProvidedThenRedirectToClient() throws Exception {
// See RFC 7636: Appendix B. Example for the S256 code_challenge_method
// https://tools.ietf.org/html/rfc7636#appendix-B
final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
this.spring.register(AuthorizationServerConfiguration.class).autowire();
ClientSettings settings = new ClientSettings();
RegisteredClient registeredClient = TestRegisteredClients
.registeredClient()
.clientSettings(settings.requireProofKey(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("code_challenge", S256_CODE_CHALLENGE)
.param("code_challenge_method", "S256")
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
assertThat(mvcResult.getResponse().getRedirectedUrl())
.doesNotContain("error=")
.contains("code=");
String authorizationCode = UriUtils.decode(UriComponentsBuilder.fromHttpUrl(mvcResult.getResponse().getRedirectedUrl())
.build()
.getQueryParams()
.getFirst("code"), "utf-8");
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorizationCode))
.param("code_verifier", S256_CODE_VERIFIER))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$.access_token").isNotEmpty());
}
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
parameters.set(OAuth2ParameterNames.STATE, "state");
return parameters;
}
private static MultiValueMap<String, String> getTokenRequestParameters(RegisteredClient registeredClient,
String authorizationCode) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
parameters.set(OAuth2ParameterNames.CODE, authorizationCode);
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
return parameters;
}
@EnableWebSecurity
static class AuthorizationServerConfiguration {
@Bean
RegisteredClientRepository registeredClientRepository() {
return registeredClientRepository;
}
@Bean
OAuth2AuthorizationService authorizationService() {
return new InMemoryOAuth2AuthorizationService();
}
@Bean
KeyManager keyManager() {
return new StaticKeyGeneratingKeyManager();
}
@Bean
WebSecurityConfigurer<WebSecurity> defaultOAuth2AuthorizationServerSecurity() {
return new WebSecurityConfigurerAdapter() {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.permitAll()
.and()
.csrf()
.disable()
.apply(new OAuth2AuthorizationServerConfigurer<>());
}
};
}
}
}

View File

@@ -21,6 +21,8 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
/**
* @author Joe Grandja
@@ -32,12 +34,17 @@ public class TestOAuth2Authorizations {
}
public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient) {
return authorization(registeredClient, Collections.emptyMap());
}
public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient, Map<String, Object> additionalParameters) {
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300));
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://provider.com/oauth2/authorize")
.clientId(registeredClient.getClientId())
.redirectUri(registeredClient.getRedirectUris().iterator().next())
.additionalParameters(additionalParameters)
.state("state")
.build();
return OAuth2Authorization.withRegisteredClient(registeredClient)

View File

@@ -22,6 +22,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.jose.JoseHeaderNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
@@ -35,6 +36,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.ClientSettings;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@@ -53,7 +59,19 @@ import static org.mockito.Mockito.when;
* @author Joe Grandja
*/
public class OAuth2AuthorizationCodeAuthenticationProviderTests {
private final String PLAIN_CODE_CHALLENGE = "pkce-key";
private final String PLAIN_CODE_VERIFIER = PLAIN_CODE_CHALLENGE;
// See RFC 7636: Appendix B. Example for the S256 code_challenge_method
// https://tools.ietf.org/html/rfc7636#appendix-B
private final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
private final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
private final String AUTHORIZATION_CODE = "code";
private RegisteredClient registeredClient;
private RegisteredClient otherRegisteredClient;
private RegisteredClient registeredClientRequiresProofKey;
private RegisteredClientRepository registeredClientRepository;
private OAuth2AuthorizationService authorizationService;
private JwtEncoder jwtEncoder;
@@ -62,7 +80,17 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
@Before
public void setUp() {
this.registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository = new InMemoryRegisteredClientRepository(this.registeredClient);
this.otherRegisteredClient = TestRegisteredClients.registeredClient2().build();
this.registeredClientRequiresProofKey = TestRegisteredClients.registeredClient()
.id("registration-3")
.clientId("client-3")
.clientSettings(new ClientSettings().requireProofKey(true))
.build();
this.registeredClientRepository = new InMemoryRegisteredClientRepository(
this.registeredClient,
this.otherRegisteredClient,
this.registeredClientRequiresProofKey
);
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.jwtEncoder = mock(JwtEncoder.class);
this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
@@ -100,7 +128,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
TestingAuthenticationToken clientPrincipal = new TestingAuthenticationToken(
this.registeredClient.getClientId(), this.registeredClient.getClientSecret());
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null);
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
@@ -113,7 +141,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
this.registeredClient.getClientId(), this.registeredClient.getClientSecret());
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null);
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
@@ -125,7 +153,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
public void authenticateWhenInvalidCodeThenThrowOAuth2AuthenticationException() {
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(this.registeredClient);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null);
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
@@ -142,7 +170,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
TestRegisteredClients.registeredClient2().build());
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null);
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
@@ -160,7 +188,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, authorizationRequest.getRedirectUri() + "-invalid");
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, authorizationRequest.getRedirectUri() + "-invalid", null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
@@ -178,16 +206,9 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, authorizationRequest.getRedirectUri());
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, authorizationRequest.getRedirectUri(), null);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
Jwt jwt = Jwt.withTokenValue("token")
.header(JoseHeaderNames.ALG, SignatureAlgorithm.RS256.getName())
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.build();
when(this.jwtEncoder.encode(any(), any())).thenReturn(jwt);
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -201,4 +222,290 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(updatedAuthorization.getAccessToken()).isNotNull();
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
}
@Test
public void authenticateWhenRequireProofKeyAndMissingPkceCodeChallengeInAuthorizationRequestThenThrowOAuth2AuthenticationException() {
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClientRequiresProofKey).build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(
AUTHORIZATION_CODE,
registeredClientRequiresProofKey.getClientId(),
authorizationRequest.getRedirectUri(),
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenRequireProofKeyAndUnsupportedCodeChallengeMethodInAuthorizationRequestThenThrowOAuth2AuthenticationException() {
Map<String, Object> pkceParameters = new HashMap<>();
pkceParameters.put(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE);
// This should never happen: the Authorization endpoint should not allow it
pkceParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-challenge-method");
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClientRequiresProofKey, pkceParameters)
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(
AUTHORIZATION_CODE,
registeredClientRequiresProofKey.getClientId(),
authorizationRequest.getRedirectUri(),
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
}
@Test
public void authenticateWhenPublicClientAndClientIdNotMatchingThrowOAuth2AuthenticationException() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, getPkceAuthorizationParametersPlain())
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(
AUTHORIZATION_CODE,
otherRegisteredClient.getClientId(),
authorizationRequest.getRedirectUri(),
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenPublicClientAndUnknownClientIdThrowOAuth2AuthenticationException() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, getPkceAuthorizationParametersPlain())
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(
AUTHORIZATION_CODE,
"invalid-client-id",
authorizationRequest.getRedirectUri(),
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_CHALLENGE)
);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
}
@Test
public void authenticateWhenPublicClientAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, getPkceAuthorizationParametersPlain())
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken(
AUTHORIZATION_CODE,
authorizationRequest.getClientId(),
authorizationRequest.getRedirectUri(),
null
);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenPrivateClientAndRequireProofKeyAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, getPkceAuthorizationParametersPlain())
.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
);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenPublicClientAndPlainMethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, getPkceAuthorizationParametersPlain())
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2AuthorizationCodeAuthenticationToken authentication = makeAuthorizationCodeAuthenticationToken("invalid-code-verifier");
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenPublicClientAndS256MethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, getPkceAuthorizationParametersS256())
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
OAuth2AuthorizationCodeAuthenticationToken authentication = makeAuthorizationCodeAuthenticationToken("invalid-code-verifier");
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
@Test
public void authenticateWhenPublicClientAndPlainMethodAndValidCodeVerifierThenReturnAccessToken() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, getPkceAuthorizationParametersPlain())
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
OAuth2AuthorizationCodeAuthenticationToken authentication = makeAuthorizationCodeAuthenticationToken(PLAIN_CODE_VERIFIER);
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
OAuth2ClientAuthenticationToken clientAuthenticationToken = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
assertThat(clientAuthenticationToken.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
assertThat(updatedAuthorization.getAccessToken()).isNotNull();
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
}
@Test
public void authenticateWhenPublicClientAndNoMethodThenDefaultToPlainAndReturnAccessToken() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, Collections.singletonMap(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE))
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
OAuth2AuthorizationCodeAuthenticationToken authentication = makeAuthorizationCodeAuthenticationToken(PLAIN_CODE_VERIFIER);
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
OAuth2ClientAuthenticationToken clientAuthenticationToken = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
assertThat(clientAuthenticationToken.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
assertThat(updatedAuthorization.getAccessToken()).isNotNull();
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
}
@Test
public void authenticateWhenPublicClientAndS256MethodAndValidCodeVerifierThenReturnAccessToken() {
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, getPkceAuthorizationParametersS256())
.build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
.thenReturn(authorization);
when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
OAuth2AuthorizationCodeAuthenticationToken authentication = makeAuthorizationCodeAuthenticationToken(S256_CODE_VERIFIER);
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
OAuth2ClientAuthenticationToken clientAuthenticationToken = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
assertThat(clientAuthenticationToken.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
assertThat(updatedAuthorization.getAccessToken()).isNotNull();
assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
}
private Map<String, Object> getPkceAuthorizationParametersPlain() {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain");
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE);
return additionalParameters;
}
private Map<String, Object> getPkceAuthorizationParametersS256() {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE);
return additionalParameters;
}
private OAuth2AuthorizationCodeAuthenticationToken makeAuthorizationCodeAuthenticationToken(String codeVerifier) {
return new OAuth2AuthorizationCodeAuthenticationToken(
AUTHORIZATION_CODE,
registeredClient.getClientId(),
registeredClient.getRedirectUris().iterator().next(),
Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, codeVerifier)
);
}
private Jwt createJwt() {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
return Jwt.withTokenValue("token")
.header(JoseHeaderNames.ALG, SignatureAlgorithm.RS256.getName())
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.build();
}
}

View File

@@ -19,6 +19,9 @@ import org.junit.Test;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import java.util.Collections;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -29,28 +32,30 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
*/
public class OAuth2AuthorizationCodeAuthenticationTokenTests {
private String code = "code";
private String clientPrincipalClientId = "clientPrincipal.clientId";
private OAuth2ClientAuthenticationToken clientPrincipal =
new OAuth2ClientAuthenticationToken(TestRegisteredClients.registeredClient().build());
new OAuth2ClientAuthenticationToken(TestRegisteredClients.registeredClient().clientId(clientPrincipalClientId).build());
private String clientId = "clientId";
private String redirectUri = "redirectUri";
private Map<String, Object> additonalParams = Collections.singletonMap("some_key", "some_value");
@Test
public void constructorWhenCodeNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(null, this.clientPrincipal, this.redirectUri))
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(null, this.clientPrincipal, this.redirectUri, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("code cannot be empty");
}
@Test
public void constructorWhenClientPrincipalNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (Authentication) null, this.redirectUri))
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (Authentication) null, this.redirectUri, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("clientPrincipal cannot be null");
}
@Test
public void constructorWhenClientIdNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (String) null, this.redirectUri))
assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (String) null, this.redirectUri, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("clientId cannot be empty");
}
@@ -58,20 +63,50 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
@Test
public void constructorWhenClientPrincipalProvidedThenCreated() {
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
this.code, this.clientPrincipal, this.redirectUri);
this.code, this.clientPrincipal, this.redirectUri, this.additonalParams);
assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal);
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.getCode()).isEqualTo(this.code);
assertThat(authentication.getRedirectUri()).isEqualTo(this.redirectUri);
assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additonalParams);
}
@Test
public void constructorWhenClientIdProvidedThenCreated() {
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
this.code, this.clientId, this.redirectUri);
this.code, this.clientId, this.redirectUri, this.additonalParams);
assertThat(authentication.getPrincipal()).isEqualTo(this.clientId);
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.getCode()).isEqualTo(this.code);
assertThat(authentication.getRedirectUri()).isEqualTo(this.redirectUri);
assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additonalParams);
}
@Test
public void getAdditionalParamsIsImmutableMap() {
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
this.code, this.clientId, this.redirectUri, this.additonalParams);
assertThatThrownBy(() -> authentication.getAdditionalParameters().put("another_key", 1))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> authentication.getAdditionalParameters().remove("some_key"))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> authentication.getAdditionalParameters().clear())
.isInstanceOf(UnsupportedOperationException.class);
}
@Test
public void getClientIdFromClientId() {
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
this.code, this.clientId, this.redirectUri, this.additonalParams);
assertThat(authentication.getClientId()).isEqualTo(this.clientId);
}
@Test
public void getClientIdFromOAuth2ClientAuthenticationTokenPrincipal() {
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
this.code, this.clientPrincipal, this.redirectUri, this.additonalParams);
assertThat(authentication.getClientId()).isEqualTo(this.clientPrincipalClientId);
}
}

View File

@@ -30,12 +30,14 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
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.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.ClientSettings;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
@@ -280,6 +282,176 @@ public class OAuth2AuthorizationEndpointFilterTests {
"state=state");
}
@Test
public void doFilterWhenProofKeyRequiredAndMissingPkceCodeChallengeThenThrowError() throws Exception {
RegisteredClient registeredClient = createClientRequireProofKey();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request = addPkceParameters(request);
request.removeParameter(PkceParameterNames.CODE_CHALLENGE);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?" +
"error=invalid_request&" +
"error_description=OAuth%202.0%20Parameter:%20code_challenge&" +
"error_uri=https://tools.ietf.org/html/rfc7636%23section-4.4.1&" +
"state=state");
}
@Test
public void doFilterWhenProofKeyRequiredAndMultiplePkceCodeChallengeThenThrowError() throws Exception {
RegisteredClient registeredClient = createClientRequireProofKey();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request = addPkceParameters(request);
request.addParameter(PkceParameterNames.CODE_CHALLENGE, "another-code-challenger");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?" +
"error=invalid_request&" +
"error_description=OAuth%202.0%20Parameter:%20code_challenge&" +
"error_uri=https://tools.ietf.org/html/rfc7636%23section-4.4.1&" +
"state=state");
}
@Test
public void doFilterWhenProofKeyNotRequiredClientAndMultiplePkceCodeChallengeThenThrowError() throws Exception {
RegisteredClient registeredClient = createClientDoNotRequireProofKey();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request = addPkceParameters(request);
request.addParameter(PkceParameterNames.CODE_CHALLENGE, "another-code-challenger");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?" +
"error=invalid_request&" +
"error_description=OAuth%202.0%20Parameter:%20code_challenge&" +
"error_uri=https://tools.ietf.org/html/rfc7636%23section-4.4.1&" +
"state=state");
}
@Test
public void doFilterWhenProofKeyRequiredAndMultiplePkceCodeChallengeMethodThenThrowError() throws Exception {
RegisteredClient registeredClient = createClientRequireProofKey();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request = addPkceParameters(request);
request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?" +
"error=invalid_request&" +
"error_description=OAuth%202.0%20Parameter:%20code_challenge_method&" +
"error_uri=https://tools.ietf.org/html/rfc7636%23section-4.4.1&" +
"state=state");
}
@Test
public void doFilterWhenProofKeyNotRequiredClientAndPkceCodeChallengeAnMultiplePkceCodeChallengeMethodThenThrowError() throws Exception {
RegisteredClient registeredClient = createClientDoNotRequireProofKey();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request = addPkceParameters(request);
request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?" +
"error=invalid_request&" +
"error_description=OAuth%202.0%20Parameter:%20code_challenge_method&" +
"error_uri=https://tools.ietf.org/html/rfc7636%23section-4.4.1&" +
"state=state");
}
@Test
public void doFilterWhenProofKeyRequiredAndUnsupportedPkceCodeChallengeMethodThenThrowError() throws Exception {
RegisteredClient registeredClient = createClientRequireProofKey();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request = addPkceParameters(request);
request.setParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-transform");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?" +
"error=invalid_request&" +
"error_description=OAuth%202.0%20Parameter:%20code_challenge_method&" +
"error_uri=https://tools.ietf.org/html/rfc7636%23section-4.4.1&" +
"state=state");
}
@Test
public void doFilterWhenProofKeyNotRequiredClientAndPkceCodeChallengeAndUnsupportedPkceCodeChallengeMethodThenThrowError() throws Exception {
RegisteredClient registeredClient = createClientDoNotRequireProofKey();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request = addPkceParameters(request);
request.setParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-transform");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?" +
"error=invalid_request&" +
"error_description=OAuth%202.0%20Parameter:%20code_challenge_method&" +
"error_uri=https://tools.ietf.org/html/rfc7636%23section-4.4.1&" +
"state=state");
}
@Test
public void doFilterWhenAuthorizationRequestValidNotAuthenticatedThenContinueChainToCommenceAuthentication() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
@@ -337,6 +509,40 @@ public class OAuth2AuthorizationEndpointFilterTests {
assertThat(authorizationRequest.getAdditionalParameters()).isEmpty();
}
@Test
public void doFilterWhenProofKeyRequiredAndAuthorizationRequestValidThenAuthorizationResponse() throws Exception {
RegisteredClient registeredClient = createClientRequireProofKey();
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
.thenReturn(registeredClient);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
request = addPkceParameters(request);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?code=.{15,}&state=state");
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization authorization = authorizationCaptor.getValue();
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authorizationRequest.getAdditionalParameters())
.size()
.isEqualTo(2)
.returnToMap()
.containsEntry(PkceParameterNames.CODE_CHALLENGE, "code-challenge")
.containsEntry(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
}
private void doFilterWhenAuthorizationRequestInvalidParameterThenError(RegisteredClient registeredClient,
String parameterName, String errorCode) throws Exception {
doFilterWhenAuthorizationRequestInvalidParameterThenError(registeredClient, parameterName, errorCode, request -> {});
@@ -374,4 +580,30 @@ public class OAuth2AuthorizationEndpointFilterTests {
return request;
}
private static MockHttpServletRequest addPkceParameters(MockHttpServletRequest request) {
request.addParameter(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
return request;
}
private RegisteredClient createClientRequireProofKey() {
ClientSettings clientSettings = new ClientSettings();
clientSettings.requireProofKey(true);
return TestRegisteredClients.registeredClient()
.clientSettings(clientSettings)
.build();
}
private RegisteredClient createClientDoNotRequireProofKey() {
ClientSettings clientSettings = new ClientSettings();
clientSettings.requireProofKey(false);
return TestRegisteredClients.registeredClient()
.clientSettings(clientSettings)
.build();
}
}

View File

@@ -34,6 +34,7 @@ import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -178,6 +179,12 @@ public class OAuth2TokenEndpointFilterTests {
@Test
public void doFilterWhenTokenRequestMissingCodeThenInvalidRequestError() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(clientPrincipal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
TestRegisteredClients.registeredClient().build());
request.removeParameter(OAuth2ParameterNames.CODE);
@@ -188,6 +195,12 @@ public class OAuth2TokenEndpointFilterTests {
@Test
public void doFilterWhenTokenRequestMultipleCodeThenInvalidRequestError() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(clientPrincipal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
TestRegisteredClients.registeredClient().build());
request.addParameter(OAuth2ParameterNames.CODE, "code-2");
@@ -198,6 +211,12 @@ public class OAuth2TokenEndpointFilterTests {
@Test
public void doFilterWhenTokenRequestMultipleRedirectUriThenInvalidRequestError() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(clientPrincipal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
TestRegisteredClients.registeredClient().build());
request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "https://example2.com");
@@ -206,6 +225,34 @@ public class OAuth2TokenEndpointFilterTests {
OAuth2ParameterNames.REDIRECT_URI, OAuth2ErrorCodes.INVALID_REQUEST, request);
}
@Test
public void doFilterWhenTokenRequestNotAuthenticatedAndMissingCodeVerifierThenInvalidRequestError() throws Exception {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
TestRegisteredClients.registeredClient().build());
request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "https://example.com");
doFilterWhenTokenRequestInvalidParameterThenError(
PkceParameterNames.CODE_VERIFIER, OAuth2ErrorCodes.INVALID_REQUEST, request);
}
@Test
public void doFilterWhenTokenRequestNotAuthenticatedAndMultipleCodeVerifierThenInvalidRequestError() throws Exception {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
TestRegisteredClients.registeredClient().build());
request.addParameter(PkceParameterNames.CODE_VERIFIER, "one-verifier");
request.addParameter(PkceParameterNames.CODE_VERIFIER, "two-verifiers");
request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "https://example.com");
doFilterWhenTokenRequestInvalidParameterThenError(
PkceParameterNames.CODE_VERIFIER, OAuth2ErrorCodes.INVALID_REQUEST, request);
}
@Test
public void doFilterWhenAuthorizationCodeTokenRequestValidThenAccessTokenResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
@@ -359,6 +406,8 @@ public class OAuth2TokenEndpointFilterTests {
request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.REDIRECT_URI, redirectUris[0]);
// The client does not need to send the client ID param, but we are resilient in case they do
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
return request;
}