Переглянути джерело

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
Stephen Doxsee 6 роки тому
батько
коміт
7739a0e91a
15 змінених файлів з 443 додано та 57 видалено
  1. 13 3
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverter.java
  2. 26 8
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java
  3. 6 1
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java
  4. 5 2
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java
  5. 44 3
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java
  6. 42 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java
  7. 71 23
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverterTests.java
  8. 54 1
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java
  9. 36 1
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java
  10. 20 11
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java
  11. 47 1
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java
  12. 23 1
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java
  13. 7 1
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java
  14. 43 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceParameterNames.java
  15. 6 1
      oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClientAuthenticationMethodTests.java

+ 13 - 3
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverter.java

@@ -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;
 	}

+ 26 - 8
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java

@@ -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;
 	}

+ 6 - 1
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java

@@ -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;

+ 5 - 2
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java

@@ -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) {

+ 44 - 3
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java

@@ -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);
+	}
 }

+ 42 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java

@@ -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);
+	}
 }

+ 71 - 23
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverterTests.java

@@ -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,77 @@ 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();
+				.redirectUri("https://client.com/callback/client-1");
+
+	@SuppressWarnings("unchecked")
+	@Test
+	public void convertWhenGrantRequestValidThenConverts() {
+		ClientRegistration clientRegistration = clientRegistrationBuilder.build();
+		OAuth2AuthorizationRequest authorizationRequest = authorizationRequestBuilder.build();
+		OAuth2AuthorizationResponse authorizationResponse = authorizationResponseBuilder.build();
 		OAuth2AuthorizationExchange authorizationExchange =
 				new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
-		this.authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest(
+		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)).startsWith("Basic ");
+
+		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.CLIENT_ID)).isNull();
+		assertThat(formParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI)).isEqualTo(
+				clientRegistration.getRedirectUriTemplate());
 	}
 
 	@SuppressWarnings("unchecked")
 	@Test
-	public void convertWhenGrantRequestValidThenConverts() {
-		RequestEntity<?> requestEntity = this.converter.convert(this.authorizationCodeGrantRequest);
+	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();
 
-		ClientRegistration clientRegistration = this.authorizationCodeGrantRequest.getClientRegistration();
+		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(
@@ -91,7 +137,7 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
 		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)).startsWith("Basic ");
+		assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isNull();
 
 		MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
 		assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo(
@@ -99,5 +145,7 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
 		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");
 	}
 }

+ 54 - 1
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java

@@ -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);
+	}
 }

+ 36 - 1
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java

@@ -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)

+ 20 - 11
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java

@@ -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) {

+ 47 - 1
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java

@@ -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}");
+	}
 }

+ 23 - 1
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java

@@ -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}");
+	}
 }

+ 7 - 1
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java

@@ -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;
 
 	/**

+ 43 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceParameterNames.java

@@ -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";
+}

+ 6 - 1
oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClientAuthenticationMethodTests.java

@@ -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");
+	}
 }