Add PKCE OAuth2 client support
- Support has been added for "RFC7636: Proof Key for Code Exchange by OAuth Public Clients" (PKCE, pronounced "pixy") to mitigate against attacks targeting the interception of the authorization code - PkceParameterNames was added for the 3 additional parameters used by PKCE (i.e. code_verifier, code_challenge, and code_challenge_method) - Default code_verifier length has been set to 128 characters--the maximum allowed by RFC7636 - ClientAuthenticationMethod.NONE was added to allow clients to request tokens without providing a client secret Fixes gh-6446
This commit is contained in:
parent
2b960b074b
commit
7739a0e91a
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -23,6 +23,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
|||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
@ -74,11 +75,20 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverter implements Conve
|
|||
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
|
||||
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
|
||||
formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
|
||||
formParameters.add(OAuth2ParameterNames.REDIRECT_URI, authorizationExchange.getAuthorizationRequest().getRedirectUri());
|
||||
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
|
||||
String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER);
|
||||
if (redirectUri != null) {
|
||||
formParameters.add(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
|
||||
}
|
||||
if (!ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
|
||||
}
|
||||
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
|
||||
}
|
||||
if (codeVerifier != null) {
|
||||
formParameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier);
|
||||
}
|
||||
|
||||
return formParameters;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -18,9 +18,12 @@ package org.springframework.security.oauth2.client.endpoint;
|
|||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -44,6 +47,7 @@ import static org.springframework.security.oauth2.core.web.reactive.function.OAu
|
|||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">Section 4.2 Client Creates the Code Challenge</a>
|
||||
*/
|
||||
public class WebClientReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
|
||||
private WebClient webClient = WebClient.builder()
|
||||
|
@ -63,12 +67,16 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient implements Re
|
|||
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
|
||||
OAuth2AuthorizationExchange authorizationExchange = authorizationGrantRequest.getAuthorizationExchange();
|
||||
String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
|
||||
BodyInserters.FormInserter<String> body = body(authorizationExchange);
|
||||
BodyInserters.FormInserter<String> body = body(authorizationExchange, clientRegistration);
|
||||
|
||||
return this.webClient.post()
|
||||
.uri(tokenUri)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()))
|
||||
.headers(headers -> {
|
||||
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
|
||||
}
|
||||
})
|
||||
.body(body)
|
||||
.exchange()
|
||||
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
|
||||
|
@ -83,14 +91,24 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient implements Re
|
|||
});
|
||||
}
|
||||
|
||||
private static BodyInserters.FormInserter<String> body(OAuth2AuthorizationExchange authorizationExchange) {
|
||||
private static BodyInserters.FormInserter<String> body(OAuth2AuthorizationExchange authorizationExchange, ClientRegistration clientRegistration) {
|
||||
OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse();
|
||||
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
|
||||
BodyInserters.FormInserter<String> body = BodyInserters
|
||||
.fromFormData("grant_type", AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.with("code", authorizationResponse.getCode());
|
||||
.fromFormData(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.with(OAuth2ParameterNames.CODE, authorizationResponse.getCode());
|
||||
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
|
||||
String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER);
|
||||
if (redirectUri != null) {
|
||||
body.with("redirect_uri", redirectUri);
|
||||
body.with(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
|
||||
}
|
||||
if (!ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
|
||||
}
|
||||
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
|
||||
}
|
||||
if (codeVerifier != null) {
|
||||
body.with(PkceParameterNames.CODE_VERIFIER, codeVerifier);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -500,6 +500,11 @@ public final class ClientRegistration implements Serializable {
|
|||
clientRegistration.clientId = this.clientId;
|
||||
clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : "";
|
||||
clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod;
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType) &&
|
||||
!StringUtils.hasText(this.clientSecret)) {
|
||||
clientRegistration.clientAuthenticationMethod = ClientAuthenticationMethod.NONE;
|
||||
}
|
||||
|
||||
clientRegistration.authorizationGrantType = this.authorizationGrantType;
|
||||
clientRegistration.redirectUriTemplate = this.redirectUriTemplate;
|
||||
clientRegistration.scopes = this.scopes;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -117,7 +117,10 @@ public final class ClientRegistrations {
|
|||
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
|
||||
return ClientAuthenticationMethod.POST;
|
||||
}
|
||||
throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods);
|
||||
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) {
|
||||
return ClientAuthenticationMethod.NONE;
|
||||
}
|
||||
throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods);
|
||||
}
|
||||
|
||||
private static List<String> getScopes(OIDCProviderMetadata metadata) {
|
||||
|
|
|
@ -20,14 +20,19 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
|||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
@ -52,6 +57,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
|
|||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
private final AntPathRequestMatcher authorizationRequestMatcher;
|
||||
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
||||
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
|
||||
/**
|
||||
* Constructs a {@code DefaultOAuth2AuthorizationRequestResolver} using the provided parameters.
|
||||
|
@ -102,9 +108,17 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
|
|||
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
|
||||
}
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
|
||||
|
||||
OAuth2AuthorizationRequest.Builder builder;
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||
builder = OAuth2AuthorizationRequest.authorizationCode();
|
||||
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
addPkceParameters(attributes, additionalParameters);
|
||||
builder.additionalParameters(additionalParameters);
|
||||
}
|
||||
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||
builder = OAuth2AuthorizationRequest.implicit();
|
||||
} else {
|
||||
|
@ -115,9 +129,6 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
|
|||
|
||||
String redirectUriStr = this.expandRedirectUri(request, clientRegistration, redirectUriAction);
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = builder
|
||||
.clientId(clientRegistration.getClientId())
|
||||
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||
|
@ -156,4 +167,34 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
|
|||
.buildAndExpand(uriVariables)
|
||||
.toUriString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
|
||||
*
|
||||
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
|
||||
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
|
||||
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
|
||||
*
|
||||
* @since 5.2
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
|
||||
*/
|
||||
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
|
||||
String codeVerifier = this.codeVerifierGenerator.generateKey();
|
||||
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
|
||||
try {
|
||||
String codeChallenge = createCodeChallenge(codeVerifier);
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
|
||||
}
|
||||
}
|
||||
|
||||
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,10 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
|||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -34,6 +36,9 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
@ -68,6 +73,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
|
|||
|
||||
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
||||
|
||||
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
* @param clientRegistrationRepository the repository to resolve the {@link ClientRegistration}
|
||||
|
@ -124,6 +131,11 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
|
|||
OAuth2AuthorizationRequest.Builder builder;
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||
builder = OAuth2AuthorizationRequest.authorizationCode();
|
||||
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
addPkceParameters(attributes, additionalParameters);
|
||||
builder.additionalParameters(additionalParameters);
|
||||
}
|
||||
}
|
||||
else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||
builder = OAuth2AuthorizationRequest.implicit();
|
||||
|
@ -164,4 +176,34 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
|
|||
.buildAndExpand(uriVariables)
|
||||
.toUriString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
|
||||
*
|
||||
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
|
||||
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
|
||||
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
|
||||
*
|
||||
* @since 5.2
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
|
||||
*/
|
||||
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
|
||||
String codeVerifier = this.codeVerifierGenerator.generateKey();
|
||||
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
|
||||
try {
|
||||
String codeChallenge = createCodeChallenge(codeVerifier);
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
|
||||
}
|
||||
}
|
||||
|
||||
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
@ -28,8 +27,14 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExch
|
|||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
|
||||
|
||||
|
@ -40,11 +45,8 @@ import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VAL
|
|||
*/
|
||||
public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
|
||||
private OAuth2AuthorizationCodeGrantRequestEntityConverter converter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
|
||||
private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("registration-1")
|
||||
private ClientRegistration.Builder clientRegistrationBuilder = ClientRegistration
|
||||
.withRegistrationId("registration-1")
|
||||
.clientId("client-1")
|
||||
.clientSecret("secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
|
||||
|
@ -55,33 +57,31 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
|
|||
.tokenUri("https://provider.com/oauth2/token")
|
||||
.userInfoUri("https://provider.com/user")
|
||||
.userNameAttributeName("id")
|
||||
.clientName("client-1")
|
||||
.build();
|
||||
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest
|
||||
.clientName("client-1");
|
||||
private OAuth2AuthorizationRequest.Builder authorizationRequestBuilder = OAuth2AuthorizationRequest
|
||||
.authorizationCode()
|
||||
.clientId(clientRegistration.getClientId())
|
||||
.clientId("client-1")
|
||||
.state("state-1234")
|
||||
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||
.redirectUri(clientRegistration.getRedirectUriTemplate())
|
||||
.scopes(clientRegistration.getScopes())
|
||||
.build();
|
||||
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponse
|
||||
.authorizationUri("https://provider.com/oauth2/authorize")
|
||||
.redirectUri("https://client.com/callback/client-1")
|
||||
.scopes(new HashSet(Arrays.asList("read", "write")));
|
||||
private OAuth2AuthorizationResponse.Builder authorizationResponseBuilder = OAuth2AuthorizationResponse
|
||||
.success("code-1234")
|
||||
.state("state-1234")
|
||||
.redirectUri(clientRegistration.getRedirectUriTemplate())
|
||||
.build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
|
||||
this.authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||
clientRegistration, authorizationExchange);
|
||||
}
|
||||
.redirectUri("https://client.com/callback/client-1");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void convertWhenGrantRequestValidThenConverts() {
|
||||
RequestEntity<?> requestEntity = this.converter.convert(this.authorizationCodeGrantRequest);
|
||||
ClientRegistration clientRegistration = clientRegistrationBuilder.build();
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorizationRequestBuilder.build();
|
||||
OAuth2AuthorizationResponse authorizationResponse = authorizationResponseBuilder.build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
|
||||
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||
clientRegistration, authorizationExchange);
|
||||
|
||||
ClientRegistration clientRegistration = this.authorizationCodeGrantRequest.getClientRegistration();
|
||||
RequestEntity<?> requestEntity = this.converter.convert(authorizationCodeGrantRequest);
|
||||
|
||||
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(requestEntity.getUrl().toASCIIString()).isEqualTo(
|
||||
|
@ -97,7 +97,55 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
|
|||
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.CODE)).isEqualTo("code-1234");
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.CLIENT_ID)).isNull();
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI)).isEqualTo(
|
||||
clientRegistration.getRedirectUriTemplate());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void convertWhenPkceGrantRequestValidThenConverts() {
|
||||
ClientRegistration clientRegistration = clientRegistrationBuilder
|
||||
.clientSecret(null)
|
||||
.build();
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(PkceParameterNames.CODE_VERIFIER, "code-verifier-1234");
|
||||
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge-1234");
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorizationRequestBuilder
|
||||
.attributes(attributes)
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
|
||||
OAuth2AuthorizationResponse authorizationResponse = authorizationResponseBuilder.build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
|
||||
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest(
|
||||
clientRegistration, authorizationExchange);
|
||||
|
||||
RequestEntity<?> requestEntity = this.converter.convert(authorizationCodeGrantRequest);
|
||||
|
||||
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(requestEntity.getUrl().toASCIIString()).isEqualTo(
|
||||
clientRegistration.getProviderDetails().getTokenUri());
|
||||
|
||||
HttpHeaders headers = requestEntity.getHeaders();
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
|
||||
assertThat(headers.getContentType()).isEqualTo(
|
||||
MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
|
||||
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isNull();
|
||||
|
||||
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.CODE)).isEqualTo("code-1234");
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI)).isEqualTo(
|
||||
clientRegistration.getRedirectUriTemplate());
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.CLIENT_ID)).isEqualTo("client-1");
|
||||
assertThat(formParameters.getFirst(PkceParameterNames.CODE_VERIFIER)).isEqualTo("code-verifier-1234");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -32,9 +32,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenRespon
|
|||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
@ -84,6 +87,9 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests {
|
|||
Instant expiresAtBefore = Instant.now().plusSeconds(3600);
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest()).block();
|
||||
String body = this.server.takeRequest().getBody().readUtf8();
|
||||
|
||||
assertThat(body).isEqualTo("grant_type=authorization_code&code=code&redirect_uri=%7BbaseUrl%7D%2F%7Baction%7D%2Foauth2%2Fcode%2F%7BregistrationId%7D");
|
||||
|
||||
Instant expiresAtAfter = Instant.now().plusSeconds(3600);
|
||||
|
||||
|
@ -288,4 +294,51 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests {
|
|||
|
||||
verify(customClient, atLeastOnce()).post();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTokenResponseWhenOAuth2AuthorizationRequestContainsPkceParametersThenTokenRequestBodyShouldContainCodeVerifier() throws Exception {
|
||||
String accessTokenSuccessResponse = "{\n" +
|
||||
" \"access_token\": \"access-token-1234\",\n" +
|
||||
" \"token_type\": \"bearer\",\n" +
|
||||
" \"expires_in\": \"3600\"\n" +
|
||||
"}\n";
|
||||
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
||||
|
||||
this.tokenResponseClient.getTokenResponse(pkceAuthorizationCodeGrantRequest()).block();
|
||||
String body = this.server.takeRequest().getBody().readUtf8();
|
||||
|
||||
assertThat(body).isEqualTo("grant_type=authorization_code&code=code&redirect_uri=%7BbaseUrl%7D%2F%7Baction%7D%2Foauth2%2Fcode%2F%7BregistrationId%7D&client_id=client-id&code_verifier=code-verifier-1234");
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationCodeGrantRequest pkceAuthorizationCodeGrantRequest() {
|
||||
ClientRegistration registration = this.clientRegistration
|
||||
.clientSecret(null)
|
||||
.build();
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(PkceParameterNames.CODE_VERIFIER, "code-verifier-1234");
|
||||
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge-1234");
|
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest
|
||||
.authorizationCode()
|
||||
.clientId(registration.getClientId())
|
||||
.state("state")
|
||||
.authorizationUri(registration.getProviderDetails().getAuthorizationUri())
|
||||
.redirectUri(registration.getRedirectUriTemplate())
|
||||
.scopes(registration.getScopes())
|
||||
.attributes(attributes)
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponse
|
||||
.success("code")
|
||||
.state("state")
|
||||
.redirectUri(registration.getRedirectUriTemplate())
|
||||
.build();
|
||||
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(authorizationRequest,
|
||||
authorizationResponse);
|
||||
return new OAuth2AuthorizationCodeGrantRequest(registration, authorizationExchange);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -174,6 +174,41 @@ public class ClientRegistrationTests {
|
|||
assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodNotProvidedAndClientSecretNullThenDefaultToNone() {
|
||||
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||
.clientId(CLIENT_ID)
|
||||
.clientSecret(null)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUriTemplate(REDIRECT_URI)
|
||||
.scope(SCOPES.toArray(new String[0]))
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.userInfoAuthenticationMethod(AuthenticationMethod.FORM)
|
||||
.jwkSetUri(JWK_SET_URI)
|
||||
.clientName(CLIENT_NAME)
|
||||
.build();
|
||||
assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodNotProvidedAndClientSecretBlankThenDefaultToNone() {
|
||||
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||
.clientId(CLIENT_ID)
|
||||
.clientSecret(" ")
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUriTemplate(REDIRECT_URI)
|
||||
.scope(SCOPES.toArray(new String[0]))
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.userInfoAuthenticationMethod(AuthenticationMethod.FORM)
|
||||
.jwkSetUri(JWK_SET_URI)
|
||||
.clientName(CLIENT_NAME)
|
||||
.build();
|
||||
assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE);
|
||||
assertThat(clientRegistration.getClientSecret()).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void buildWhenAuthorizationCodeGrantRedirectUriIsNullThenThrowIllegalArgumentException() {
|
||||
ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -92,7 +92,8 @@ public class ClientRegistrationsTest {
|
|||
+ " \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n"
|
||||
+ " \"token_endpoint_auth_methods_supported\": [\n"
|
||||
+ " \"client_secret_post\", \n"
|
||||
+ " \"client_secret_basic\"\n"
|
||||
+ " \"client_secret_basic\", \n"
|
||||
+ " \"none\"\n"
|
||||
+ " ], \n"
|
||||
+ " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n"
|
||||
+ "}";
|
||||
|
@ -119,7 +120,7 @@ public class ClientRegistrationsTest {
|
|||
|
||||
@Test
|
||||
public void issuerWhenAllInformationThenSuccess() throws Exception {
|
||||
ClientRegistration registration = registration("");
|
||||
ClientRegistration registration = registration("").build();
|
||||
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
|
||||
|
||||
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
|
||||
|
@ -154,7 +155,7 @@ public class ClientRegistrationsTest {
|
|||
public void issuerWhenScopesNullThenScopesDefaulted() throws Exception {
|
||||
this.response.remove("scopes_supported");
|
||||
|
||||
ClientRegistration registration = registration("");
|
||||
ClientRegistration registration = registration("").build();
|
||||
|
||||
assertThat(registration.getScopes()).containsOnly("openid");
|
||||
}
|
||||
|
@ -163,7 +164,7 @@ public class ClientRegistrationsTest {
|
|||
public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception {
|
||||
this.response.remove("grant_types_supported");
|
||||
|
||||
ClientRegistration registration = registration("");
|
||||
ClientRegistration registration = registration("").build();
|
||||
|
||||
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
|
@ -184,7 +185,7 @@ public class ClientRegistrationsTest {
|
|||
public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception {
|
||||
this.response.remove("token_endpoint_auth_methods_supported");
|
||||
|
||||
ClientRegistration registration = registration("");
|
||||
ClientRegistration registration = registration("").build();
|
||||
|
||||
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
|
||||
}
|
||||
|
@ -193,11 +194,20 @@ public class ClientRegistrationsTest {
|
|||
public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post"));
|
||||
|
||||
ClientRegistration registration = registration("");
|
||||
ClientRegistration registration = registration("").build();
|
||||
|
||||
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void issuerWhenTokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception {
|
||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none"));
|
||||
|
||||
ClientRegistration registration = registration("").build();
|
||||
|
||||
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* We currently only support client_secret_basic, so verify we have a meaningful error until we add support.
|
||||
* @throws Exception
|
||||
|
@ -208,7 +218,7 @@ public class ClientRegistrationsTest {
|
|||
|
||||
assertThatThrownBy(() -> registration(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]");
|
||||
.hasMessageContaining("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -229,7 +239,7 @@ public class ClientRegistrationsTest {
|
|||
.hasMessageContaining("The Issuer \"https://example.com\" provided in the OpenID Configuration did not match the requested issuer \"" + this.issuer + "\"");
|
||||
}
|
||||
|
||||
private ClientRegistration registration(String path) throws Exception {
|
||||
private ClientRegistration.Builder registration(String path) throws Exception {
|
||||
this.issuer = createIssuerFromServer(path);
|
||||
this.response.put("issuer", this.issuer);
|
||||
String body = this.mapper.writeValueAsString(this.response);
|
||||
|
@ -240,8 +250,7 @@ public class ClientRegistrationsTest {
|
|||
|
||||
return ClientRegistrations.fromOidcIssuerLocation(this.issuer)
|
||||
.clientId("client-id")
|
||||
.clientSecret("client-secret")
|
||||
.build();
|
||||
.clientSecret("client-secret");
|
||||
}
|
||||
|
||||
private String createIssuerFromServer(String path) {
|
||||
|
|
|
@ -23,9 +23,11 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
|||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
@ -39,6 +41,7 @@ import static org.assertj.core.api.Assertions.entry;
|
|||
public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||
private ClientRegistration registration1;
|
||||
private ClientRegistration registration2;
|
||||
private ClientRegistration pkceRegistration;
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
private String authorizationRequestBaseUri = "/oauth2/authorization";
|
||||
private DefaultOAuth2AuthorizationRequestResolver resolver;
|
||||
|
@ -47,8 +50,15 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
|||
public void setUp() {
|
||||
this.registration1 = TestClientRegistrations.clientRegistration().build();
|
||||
this.registration2 = TestClientRegistrations.clientRegistration2().build();
|
||||
this.pkceRegistration = TestClientRegistrations.clientRegistration()
|
||||
.registrationId("pkce-client-registration-id")
|
||||
.clientId("pkce-client-id")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
|
||||
.clientSecret(null)
|
||||
.build();
|
||||
|
||||
this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(
|
||||
this.registration1, this.registration2);
|
||||
this.registration1, this.registration2, this.pkceRegistration);
|
||||
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(
|
||||
this.clientRegistrationRepository, this.authorizationRequestBaseUri);
|
||||
}
|
||||
|
@ -255,4 +265,40 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
|||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenAuthorizationRequestWithValidPkceClientThenResolves() {
|
||||
ClientRegistration clientRegistration = this.pkceRegistration;
|
||||
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
|
||||
assertThat(authorizationRequest).isNotNull();
|
||||
assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(
|
||||
clientRegistration.getProviderDetails().getAuthorizationUri());
|
||||
assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE);
|
||||
assertThat(authorizationRequest.getClientId()).isEqualTo(clientRegistration.getClientId());
|
||||
assertThat(authorizationRequest.getRedirectUri())
|
||||
.isEqualTo("http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId());
|
||||
assertThat(authorizationRequest.getScopes()).isEqualTo(clientRegistration.getScopes());
|
||||
assertThat(authorizationRequest.getState()).isNotNull();
|
||||
assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(OAuth2ParameterNames.REGISTRATION_ID);
|
||||
assertThat(authorizationRequest.getAdditionalParameters()).containsKey(PkceParameterNames.CODE_CHALLENGE);
|
||||
assertThat(authorizationRequest.getAdditionalParameters())
|
||||
.contains(entry(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"));
|
||||
assertThat(authorizationRequest.getAttributes())
|
||||
.contains(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
|
||||
assertThat(authorizationRequest.getAttributes())
|
||||
.containsKey(PkceParameterNames.CODE_VERIFIER);
|
||||
assertThat((String) authorizationRequest.getAttribute(PkceParameterNames.CODE_VERIFIER)).matches("^([a-zA-Z0-9\\-\\.\\_\\~]){128}$");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=pkce-client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/pkce-client-registration-id&" +
|
||||
"code_challenge_method=S256&" +
|
||||
"code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -27,7 +27,9 @@ import org.springframework.mock.web.server.MockServerWebExchange;
|
|||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
@ -87,4 +89,24 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests {
|
|||
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path));
|
||||
return this.resolver.resolve(exchange).block();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenAuthorizationRequestWithValidPkceClientThenResolves() {
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(
|
||||
Mono.just(TestClientRegistrations.clientRegistration()
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
|
||||
.clientSecret(null)
|
||||
.build()));
|
||||
|
||||
OAuth2AuthorizationRequest request = resolve("/oauth2/authorization/registration-id");
|
||||
|
||||
assertThat((String) request.getAttribute(PkceParameterNames.CODE_VERIFIER)).matches("^([a-zA-Z0-9\\-\\.\\_\\~]){128}$");
|
||||
|
||||
assertThat(request.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.*?&" +
|
||||
"redirect_uri=/login/oauth2/code/registration-id&" +
|
||||
"code_challenge_method=S256&" +
|
||||
"code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -31,6 +31,12 @@ public final class ClientAuthenticationMethod implements Serializable {
|
|||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
public static final ClientAuthenticationMethod BASIC = new ClientAuthenticationMethod("basic");
|
||||
public static final ClientAuthenticationMethod POST = new ClientAuthenticationMethod("post");
|
||||
|
||||
/**
|
||||
* @since 5.2
|
||||
*/
|
||||
public static final ClientAuthenticationMethod NONE = new ClientAuthenticationMethod("none");
|
||||
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2002-2019 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
|
||||
*
|
||||
* http://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.core.endpoint;
|
||||
|
||||
/**
|
||||
* Standard parameter names defined in the OAuth Parameters Registry
|
||||
* and used by the authorization endpoint and token endpoint.
|
||||
*
|
||||
* @author Stephen Doxsee
|
||||
* @author Kevin Bolduc
|
||||
* @since 5.2
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-6.1">6.1 OAuth Parameters Registry</a>
|
||||
*/
|
||||
public interface PkceParameterNames {
|
||||
|
||||
/**
|
||||
* {@code code_challenge} - used in Authorization Request.
|
||||
*/
|
||||
String CODE_CHALLENGE = "code_challenge";
|
||||
|
||||
/**
|
||||
* {@code code_challenge_method} - used in Authorization Request.
|
||||
*/
|
||||
String CODE_CHALLENGE_METHOD = "code_challenge_method";
|
||||
|
||||
/**
|
||||
* {@code code_verifier} - used in Token Request.
|
||||
*/
|
||||
String CODE_VERIFIER = "code_verifier";
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
|
@ -40,4 +40,9 @@ public class ClientAuthenticationMethodTests {
|
|||
public void getValueWhenAuthenticationMethodPostThenReturnPost() {
|
||||
assertThat(ClientAuthenticationMethod.POST.getValue()).isEqualTo("post");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getValueWhenAuthenticationMethodNoneThenReturnNone() {
|
||||
assertThat(ClientAuthenticationMethod.NONE.getValue()).isEqualTo("none");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue