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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ClientAuthenticationMethod;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
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.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
@ -74,11 +75,20 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverter implements Conve
|
||||||
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
|
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
|
||||||
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
|
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
|
||||||
formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
|
formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
|
||||||
formParameters.add(OAuth2ParameterNames.REDIRECT_URI, authorizationExchange.getAuthorizationRequest().getRedirectUri());
|
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
|
||||||
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
|
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());
|
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
|
||||||
|
}
|
||||||
|
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||||
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
|
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
|
||||||
}
|
}
|
||||||
|
if (codeVerifier != null) {
|
||||||
|
formParameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier);
|
||||||
|
}
|
||||||
|
|
||||||
return formParameters;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.http.MediaType;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
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.OAuth2AccessTokenResponse;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
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.BodyInserters;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.util.Assert;
|
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://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.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/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> {
|
public class WebClientReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
|
||||||
private WebClient webClient = WebClient.builder()
|
private WebClient webClient = WebClient.builder()
|
||||||
|
@ -63,12 +67,16 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient implements Re
|
||||||
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
|
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
|
||||||
OAuth2AuthorizationExchange authorizationExchange = authorizationGrantRequest.getAuthorizationExchange();
|
OAuth2AuthorizationExchange authorizationExchange = authorizationGrantRequest.getAuthorizationExchange();
|
||||||
String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
|
String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
|
||||||
BodyInserters.FormInserter<String> body = body(authorizationExchange);
|
BodyInserters.FormInserter<String> body = body(authorizationExchange, clientRegistration);
|
||||||
|
|
||||||
return this.webClient.post()
|
return this.webClient.post()
|
||||||
.uri(tokenUri)
|
.uri(tokenUri)
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
.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)
|
.body(body)
|
||||||
.exchange()
|
.exchange()
|
||||||
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
|
.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();
|
OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse();
|
||||||
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
|
|
||||||
BodyInserters.FormInserter<String> body = BodyInserters
|
BodyInserters.FormInserter<String> body = BodyInserters
|
||||||
.fromFormData("grant_type", AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
.fromFormData(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||||
.with("code", authorizationResponse.getCode());
|
.with(OAuth2ParameterNames.CODE, authorizationResponse.getCode());
|
||||||
|
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
|
||||||
|
String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER);
|
||||||
if (redirectUri != null) {
|
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;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.clientId = this.clientId;
|
||||||
clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : "";
|
clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : "";
|
||||||
clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod;
|
clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod;
|
||||||
|
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType) &&
|
||||||
|
!StringUtils.hasText(this.clientSecret)) {
|
||||||
|
clientRegistration.clientAuthenticationMethod = ClientAuthenticationMethod.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
clientRegistration.authorizationGrantType = this.authorizationGrantType;
|
clientRegistration.authorizationGrantType = this.authorizationGrantType;
|
||||||
clientRegistration.redirectUriTemplate = this.redirectUriTemplate;
|
clientRegistration.redirectUriTemplate = this.redirectUriTemplate;
|
||||||
clientRegistration.scopes = this.scopes;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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)) {
|
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
|
||||||
return ClientAuthenticationMethod.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) {
|
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.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
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.OAuth2AuthorizationRequest;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
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.UrlUtils;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
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.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -52,6 +57,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
|
||||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||||
private final AntPathRequestMatcher authorizationRequestMatcher;
|
private final AntPathRequestMatcher authorizationRequestMatcher;
|
||||||
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
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.
|
* 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);
|
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;
|
OAuth2AuthorizationRequest.Builder builder;
|
||||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
|
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||||
builder = OAuth2AuthorizationRequest.authorizationCode();
|
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())) {
|
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||||
builder = OAuth2AuthorizationRequest.implicit();
|
builder = OAuth2AuthorizationRequest.implicit();
|
||||||
} else {
|
} else {
|
||||||
|
@ -115,9 +129,6 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
|
||||||
|
|
||||||
String redirectUriStr = this.expandRedirectUri(request, clientRegistration, redirectUriAction);
|
String redirectUriStr = this.expandRedirectUri(request, clientRegistration, redirectUriAction);
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
|
|
||||||
|
|
||||||
OAuth2AuthorizationRequest authorizationRequest = builder
|
OAuth2AuthorizationRequest authorizationRequest = builder
|
||||||
.clientId(clientRegistration.getClientId())
|
.clientId(clientRegistration.getClientId())
|
||||||
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||||
|
@ -156,4 +167,34 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
|
||||||
.buildAndExpand(uriVariables)
|
.buildAndExpand(uriVariables)
|
||||||
.toUriString();
|
.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.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
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.OAuth2AuthorizationRequest;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
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.PathPatternParserServerWebExchangeMatcher;
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -34,6 +36,9 @@ import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
import reactor.core.publisher.Mono;
|
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.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -68,6 +73,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
|
||||||
|
|
||||||
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
||||||
|
|
||||||
|
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance
|
* Creates a new instance
|
||||||
* @param clientRegistrationRepository the repository to resolve the {@link ClientRegistration}
|
* @param clientRegistrationRepository the repository to resolve the {@link ClientRegistration}
|
||||||
|
@ -124,6 +131,11 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
|
||||||
OAuth2AuthorizationRequest.Builder builder;
|
OAuth2AuthorizationRequest.Builder builder;
|
||||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
|
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||||
builder = OAuth2AuthorizationRequest.authorizationCode();
|
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())) {
|
else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||||
builder = OAuth2AuthorizationRequest.implicit();
|
builder = OAuth2AuthorizationRequest.implicit();
|
||||||
|
@ -164,4 +176,34 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
|
||||||
.buildAndExpand(uriVariables)
|
.buildAndExpand(uriVariables)
|
||||||
.toUriString();
|
.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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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;
|
package org.springframework.security.oauth2.client.endpoint;
|
||||||
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
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.OAuth2AuthorizationRequest;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||||
import org.springframework.util.MultiValueMap;
|
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.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
|
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 {
|
public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
|
||||||
private OAuth2AuthorizationCodeGrantRequestEntityConverter converter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
|
private OAuth2AuthorizationCodeGrantRequestEntityConverter converter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
|
||||||
private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest;
|
private ClientRegistration.Builder clientRegistrationBuilder = ClientRegistration
|
||||||
|
.withRegistrationId("registration-1")
|
||||||
@Before
|
|
||||||
public void setup() {
|
|
||||||
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("registration-1")
|
|
||||||
.clientId("client-1")
|
.clientId("client-1")
|
||||||
.clientSecret("secret")
|
.clientSecret("secret")
|
||||||
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
|
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
|
||||||
|
@ -55,33 +57,31 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
|
||||||
.tokenUri("https://provider.com/oauth2/token")
|
.tokenUri("https://provider.com/oauth2/token")
|
||||||
.userInfoUri("https://provider.com/user")
|
.userInfoUri("https://provider.com/user")
|
||||||
.userNameAttributeName("id")
|
.userNameAttributeName("id")
|
||||||
.clientName("client-1")
|
.clientName("client-1");
|
||||||
.build();
|
private OAuth2AuthorizationRequest.Builder authorizationRequestBuilder = OAuth2AuthorizationRequest
|
||||||
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest
|
|
||||||
.authorizationCode()
|
.authorizationCode()
|
||||||
.clientId(clientRegistration.getClientId())
|
.clientId("client-1")
|
||||||
.state("state-1234")
|
.state("state-1234")
|
||||||
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
.authorizationUri("https://provider.com/oauth2/authorize")
|
||||||
.redirectUri(clientRegistration.getRedirectUriTemplate())
|
.redirectUri("https://client.com/callback/client-1")
|
||||||
.scopes(clientRegistration.getScopes())
|
.scopes(new HashSet(Arrays.asList("read", "write")));
|
||||||
.build();
|
private OAuth2AuthorizationResponse.Builder authorizationResponseBuilder = OAuth2AuthorizationResponse
|
||||||
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponse
|
|
||||||
.success("code-1234")
|
.success("code-1234")
|
||||||
.state("state-1234")
|
.state("state-1234")
|
||||||
.redirectUri(clientRegistration.getRedirectUriTemplate())
|
.redirectUri("https://client.com/callback/client-1");
|
||||||
.build();
|
|
||||||
OAuth2AuthorizationExchange authorizationExchange =
|
|
||||||
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
|
|
||||||
this.authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest(
|
|
||||||
clientRegistration, authorizationExchange);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
public void convertWhenGrantRequestValidThenConverts() {
|
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.getMethod()).isEqualTo(HttpMethod.POST);
|
||||||
assertThat(requestEntity.getUrl().toASCIIString()).isEqualTo(
|
assertThat(requestEntity.getUrl().toASCIIString()).isEqualTo(
|
||||||
|
@ -97,7 +97,55 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
|
||||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo(
|
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo(
|
||||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.CODE)).isEqualTo("code-1234");
|
assertThat(formParameters.getFirst(OAuth2ParameterNames.CODE)).isEqualTo("code-1234");
|
||||||
|
assertThat(formParameters.getFirst(OAuth2ParameterNames.CLIENT_ID)).isNull();
|
||||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI)).isEqualTo(
|
assertThat(formParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI)).isEqualTo(
|
||||||
clientRegistration.getRedirectUriTemplate());
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.OAuth2AuthorizationExchange;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
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 org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
import java.time.Instant;
|
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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
@ -84,6 +87,9 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests {
|
||||||
Instant expiresAtBefore = Instant.now().plusSeconds(3600);
|
Instant expiresAtBefore = Instant.now().plusSeconds(3600);
|
||||||
|
|
||||||
OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest()).block();
|
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);
|
Instant expiresAtAfter = Instant.now().plusSeconds(3600);
|
||||||
|
|
||||||
|
@ -288,4 +294,51 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests {
|
||||||
|
|
||||||
verify(customClient, atLeastOnce()).post();
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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);
|
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)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void buildWhenAuthorizationCodeGrantRedirectUriIsNullThenThrowIllegalArgumentException() {
|
public void buildWhenAuthorizationCodeGrantRedirectUriIsNullThenThrowIllegalArgumentException() {
|
||||||
ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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\": \"https://example.com/oauth2/v4/token\", \n"
|
||||||
+ " \"token_endpoint_auth_methods_supported\": [\n"
|
+ " \"token_endpoint_auth_methods_supported\": [\n"
|
||||||
+ " \"client_secret_post\", \n"
|
+ " \"client_secret_post\", \n"
|
||||||
+ " \"client_secret_basic\"\n"
|
+ " \"client_secret_basic\", \n"
|
||||||
|
+ " \"none\"\n"
|
||||||
+ " ], \n"
|
+ " ], \n"
|
||||||
+ " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n"
|
+ " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n"
|
||||||
+ "}";
|
+ "}";
|
||||||
|
@ -119,7 +120,7 @@ public class ClientRegistrationsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void issuerWhenAllInformationThenSuccess() throws Exception {
|
public void issuerWhenAllInformationThenSuccess() throws Exception {
|
||||||
ClientRegistration registration = registration("");
|
ClientRegistration registration = registration("").build();
|
||||||
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
|
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
|
||||||
|
|
||||||
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
|
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
|
||||||
|
@ -154,7 +155,7 @@ public class ClientRegistrationsTest {
|
||||||
public void issuerWhenScopesNullThenScopesDefaulted() throws Exception {
|
public void issuerWhenScopesNullThenScopesDefaulted() throws Exception {
|
||||||
this.response.remove("scopes_supported");
|
this.response.remove("scopes_supported");
|
||||||
|
|
||||||
ClientRegistration registration = registration("");
|
ClientRegistration registration = registration("").build();
|
||||||
|
|
||||||
assertThat(registration.getScopes()).containsOnly("openid");
|
assertThat(registration.getScopes()).containsOnly("openid");
|
||||||
}
|
}
|
||||||
|
@ -163,7 +164,7 @@ public class ClientRegistrationsTest {
|
||||||
public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception {
|
public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception {
|
||||||
this.response.remove("grant_types_supported");
|
this.response.remove("grant_types_supported");
|
||||||
|
|
||||||
ClientRegistration registration = registration("");
|
ClientRegistration registration = registration("").build();
|
||||||
|
|
||||||
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
|
assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||||
}
|
}
|
||||||
|
@ -184,7 +185,7 @@ public class ClientRegistrationsTest {
|
||||||
public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception {
|
public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception {
|
||||||
this.response.remove("token_endpoint_auth_methods_supported");
|
this.response.remove("token_endpoint_auth_methods_supported");
|
||||||
|
|
||||||
ClientRegistration registration = registration("");
|
ClientRegistration registration = registration("").build();
|
||||||
|
|
||||||
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
|
assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
|
||||||
}
|
}
|
||||||
|
@ -193,11 +194,20 @@ public class ClientRegistrationsTest {
|
||||||
public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception {
|
public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception {
|
||||||
this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post"));
|
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);
|
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.
|
* We currently only support client_secret_basic, so verify we have a meaningful error until we add support.
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
|
@ -208,7 +218,7 @@ public class ClientRegistrationsTest {
|
||||||
|
|
||||||
assertThatThrownBy(() -> registration(""))
|
assertThatThrownBy(() -> registration(""))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.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
|
@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 + "\"");
|
.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.issuer = createIssuerFromServer(path);
|
||||||
this.response.put("issuer", this.issuer);
|
this.response.put("issuer", this.issuer);
|
||||||
String body = this.mapper.writeValueAsString(this.response);
|
String body = this.mapper.writeValueAsString(this.response);
|
||||||
|
@ -240,8 +250,7 @@ public class ClientRegistrationsTest {
|
||||||
|
|
||||||
return ClientRegistrations.fromOidcIssuerLocation(this.issuer)
|
return ClientRegistrations.fromOidcIssuerLocation(this.issuer)
|
||||||
.clientId("client-id")
|
.clientId("client-id")
|
||||||
.clientSecret("client-secret")
|
.clientSecret("client-secret");
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createIssuerFromServer(String path) {
|
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.InMemoryClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
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.OAuth2AuthorizationRequest;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
@ -39,6 +41,7 @@ import static org.assertj.core.api.Assertions.entry;
|
||||||
public class DefaultOAuth2AuthorizationRequestResolverTests {
|
public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||||
private ClientRegistration registration1;
|
private ClientRegistration registration1;
|
||||||
private ClientRegistration registration2;
|
private ClientRegistration registration2;
|
||||||
|
private ClientRegistration pkceRegistration;
|
||||||
private ClientRegistrationRepository clientRegistrationRepository;
|
private ClientRegistrationRepository clientRegistrationRepository;
|
||||||
private String authorizationRequestBaseUri = "/oauth2/authorization";
|
private String authorizationRequestBaseUri = "/oauth2/authorization";
|
||||||
private DefaultOAuth2AuthorizationRequestResolver resolver;
|
private DefaultOAuth2AuthorizationRequestResolver resolver;
|
||||||
|
@ -47,8 +50,15 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
this.registration1 = TestClientRegistrations.clientRegistration().build();
|
this.registration1 = TestClientRegistrations.clientRegistration().build();
|
||||||
this.registration2 = TestClientRegistrations.clientRegistration2().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.clientRegistrationRepository = new InMemoryClientRegistrationRepository(
|
||||||
this.registration1, this.registration2);
|
this.registration1, this.registration2, this.pkceRegistration);
|
||||||
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(
|
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(
|
||||||
this.clientRegistrationRepository, this.authorizationRequestBaseUri);
|
this.clientRegistrationRepository, this.authorizationRequestBaseUri);
|
||||||
}
|
}
|
||||||
|
@ -255,4 +265,40 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||||
"scope=read:user&state=.{15,}&" +
|
"scope=read:user&state=.{15,}&" +
|
||||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id-2");
|
"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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ClientRegistration;
|
||||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
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.OAuth2AuthorizationRequest;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -87,4 +89,24 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests {
|
||||||
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path));
|
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path));
|
||||||
return this.resolver.resolve(exchange).block();
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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;
|
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||||
public static final ClientAuthenticationMethod BASIC = new ClientAuthenticationMethod("basic");
|
public static final ClientAuthenticationMethod BASIC = new ClientAuthenticationMethod("basic");
|
||||||
public static final ClientAuthenticationMethod POST = new ClientAuthenticationMethod("post");
|
public static final ClientAuthenticationMethod POST = new ClientAuthenticationMethod("post");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 5.2
|
||||||
|
*/
|
||||||
|
public static final ClientAuthenticationMethod NONE = new ClientAuthenticationMethod("none");
|
||||||
|
|
||||||
private final String value;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -40,4 +40,9 @@ public class ClientAuthenticationMethodTests {
|
||||||
public void getValueWhenAuthenticationMethodPostThenReturnPost() {
|
public void getValueWhenAuthenticationMethodPostThenReturnPost() {
|
||||||
assertThat(ClientAuthenticationMethod.POST.getValue()).isEqualTo("post");
|
assertThat(ClientAuthenticationMethod.POST.getValue()).isEqualTo("post");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getValueWhenAuthenticationMethodNoneThenReturnNone() {
|
||||||
|
assertThat(ClientAuthenticationMethod.NONE.getValue()).isEqualTo("none");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue