Skip to content

Commit 7739a0e

Browse files
sdoxseejgrandja
authored andcommitted
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
1 parent 2b960b0 commit 7739a0e

15 files changed

+443
-57
lines changed

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

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2424
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
2525
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
26+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
2627
import org.springframework.util.LinkedMultiValueMap;
2728
import org.springframework.util.MultiValueMap;
2829
import org.springframework.web.util.UriComponentsBuilder;
@@ -74,11 +75,20 @@ private MultiValueMap<String, String> buildFormParameters(OAuth2AuthorizationCod
7475
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
7576
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
7677
formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
77-
formParameters.add(OAuth2ParameterNames.REDIRECT_URI, authorizationExchange.getAuthorizationRequest().getRedirectUri());
78-
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
78+
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
79+
String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER);
80+
if (redirectUri != null) {
81+
formParameters.add(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
82+
}
83+
if (!ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
7984
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
85+
}
86+
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
8087
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
8188
}
89+
if (codeVerifier != null) {
90+
formParameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier);
91+
}
8292

8393
return formParameters;
8494
}

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

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,9 +18,12 @@
1818
import org.springframework.http.MediaType;
1919
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2020
import org.springframework.security.oauth2.core.AuthorizationGrantType;
21+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2122
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
2223
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
2324
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
25+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
26+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
2427
import org.springframework.web.reactive.function.BodyInserters;
2528
import org.springframework.web.reactive.function.client.WebClient;
2629
import org.springframework.util.Assert;
@@ -44,6 +47,7 @@
4447
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
4548
* @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>
4649
* @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>
50+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">Section 4.2 Client Creates the Code Challenge</a>
4751
*/
4852
public class WebClientReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
4953
private WebClient webClient = WebClient.builder()
@@ -63,12 +67,16 @@ public Mono<OAuth2AccessTokenResponse> getTokenResponse(OAuth2AuthorizationCodeG
6367
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
6468
OAuth2AuthorizationExchange authorizationExchange = authorizationGrantRequest.getAuthorizationExchange();
6569
String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
66-
BodyInserters.FormInserter<String> body = body(authorizationExchange);
70+
BodyInserters.FormInserter<String> body = body(authorizationExchange, clientRegistration);
6771

6872
return this.webClient.post()
6973
.uri(tokenUri)
7074
.accept(MediaType.APPLICATION_JSON)
71-
.headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()))
75+
.headers(headers -> {
76+
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
77+
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
78+
}
79+
})
7280
.body(body)
7381
.exchange()
7482
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
@@ -83,14 +91,24 @@ public Mono<OAuth2AccessTokenResponse> getTokenResponse(OAuth2AuthorizationCodeG
8391
});
8492
}
8593

86-
private static BodyInserters.FormInserter<String> body(OAuth2AuthorizationExchange authorizationExchange) {
94+
private static BodyInserters.FormInserter<String> body(OAuth2AuthorizationExchange authorizationExchange, ClientRegistration clientRegistration) {
8795
OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse();
88-
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
8996
BodyInserters.FormInserter<String> body = BodyInserters
90-
.fromFormData("grant_type", AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
91-
.with("code", authorizationResponse.getCode());
97+
.fromFormData(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
98+
.with(OAuth2ParameterNames.CODE, authorizationResponse.getCode());
99+
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
100+
String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER);
92101
if (redirectUri != null) {
93-
body.with("redirect_uri", redirectUri);
102+
body.with(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
103+
}
104+
if (!ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
105+
body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
106+
}
107+
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
108+
body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
109+
}
110+
if (codeVerifier != null) {
111+
body.with(PkceParameterNames.CODE_VERIFIER, codeVerifier);
94112
}
95113
return body;
96114
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -500,6 +500,11 @@ private ClientRegistration create() {
500500
clientRegistration.clientId = this.clientId;
501501
clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : "";
502502
clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod;
503+
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType) &&
504+
!StringUtils.hasText(this.clientSecret)) {
505+
clientRegistration.clientAuthenticationMethod = ClientAuthenticationMethod.NONE;
506+
}
507+
503508
clientRegistration.authorizationGrantType = this.authorizationGrantType;
504509
clientRegistration.redirectUriTemplate = this.redirectUriTemplate;
505510
clientRegistration.scopes = this.scopes;

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -117,7 +117,10 @@ private static ClientAuthenticationMethod getClientAuthenticationMethod(String i
117117
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
118118
return ClientAuthenticationMethod.POST;
119119
}
120-
throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods);
120+
if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) {
121+
return ClientAuthenticationMethod.NONE;
122+
}
123+
throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods);
121124
}
122125

123126
private static List<String> getScopes(OIDCProviderMetadata metadata) {

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

+44-3
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@
2020
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2121
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
2222
import org.springframework.security.oauth2.core.AuthorizationGrantType;
23+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2324
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
2425
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
26+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
2527
import org.springframework.security.web.util.UrlUtils;
2628
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
2729
import org.springframework.util.Assert;
2830
import org.springframework.web.util.UriComponentsBuilder;
2931

3032
import javax.servlet.http.HttpServletRequest;
33+
import java.nio.charset.StandardCharsets;
34+
import java.security.MessageDigest;
35+
import java.security.NoSuchAlgorithmException;
3136
import java.util.Base64;
3237
import java.util.HashMap;
3338
import java.util.Map;
@@ -52,6 +57,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
5257
private final ClientRegistrationRepository clientRegistrationRepository;
5358
private final AntPathRequestMatcher authorizationRequestMatcher;
5459
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
60+
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
5561

5662
/**
5763
* Constructs a {@code DefaultOAuth2AuthorizationRequestResolver} using the provided parameters.
@@ -102,9 +108,17 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re
102108
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
103109
}
104110

111+
Map<String, Object> attributes = new HashMap<>();
112+
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
113+
105114
OAuth2AuthorizationRequest.Builder builder;
106115
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
107116
builder = OAuth2AuthorizationRequest.authorizationCode();
117+
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
118+
Map<String, Object> additionalParameters = new HashMap<>();
119+
addPkceParameters(attributes, additionalParameters);
120+
builder.additionalParameters(additionalParameters);
121+
}
108122
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
109123
builder = OAuth2AuthorizationRequest.implicit();
110124
} else {
@@ -115,9 +129,6 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re
115129

116130
String redirectUriStr = this.expandRedirectUri(request, clientRegistration, redirectUriAction);
117131

118-
Map<String, Object> attributes = new HashMap<>();
119-
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
120-
121132
OAuth2AuthorizationRequest authorizationRequest = builder
122133
.clientId(clientRegistration.getClientId())
123134
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
@@ -156,4 +167,34 @@ private String expandRedirectUri(HttpServletRequest request, ClientRegistration
156167
.buildAndExpand(uriVariables)
157168
.toUriString();
158169
}
170+
171+
/**
172+
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
173+
*
174+
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
175+
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
176+
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
177+
*
178+
* @since 5.2
179+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
180+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
181+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
182+
*/
183+
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
184+
String codeVerifier = this.codeVerifierGenerator.generateKey();
185+
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
186+
try {
187+
String codeChallenge = createCodeChallenge(codeVerifier);
188+
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
189+
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
190+
} catch (NoSuchAlgorithmException e) {
191+
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
192+
}
193+
}
194+
195+
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
196+
MessageDigest md = MessageDigest.getInstance("SHA-256");
197+
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
198+
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
199+
}
159200
}

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

+42
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2525
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
2626
import org.springframework.security.oauth2.core.AuthorizationGrantType;
27+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2728
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
2829
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
30+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
2931
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
3032
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
3133
import org.springframework.util.Assert;
@@ -34,6 +36,9 @@
3436
import org.springframework.web.util.UriComponentsBuilder;
3537
import reactor.core.publisher.Mono;
3638

39+
import java.nio.charset.StandardCharsets;
40+
import java.security.MessageDigest;
41+
import java.security.NoSuchAlgorithmException;
3742
import java.util.Base64;
3843
import java.util.HashMap;
3944
import java.util.Map;
@@ -68,6 +73,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
6873

6974
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
7075

76+
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
77+
7178
/**
7279
* Creates a new instance
7380
* @param clientRegistrationRepository the repository to resolve the {@link ClientRegistration}
@@ -124,6 +131,11 @@ private OAuth2AuthorizationRequest authorizationRequest(ServerWebExchange exchan
124131
OAuth2AuthorizationRequest.Builder builder;
125132
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
126133
builder = OAuth2AuthorizationRequest.authorizationCode();
134+
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
135+
Map<String, Object> additionalParameters = new HashMap<>();
136+
addPkceParameters(attributes, additionalParameters);
137+
builder.additionalParameters(additionalParameters);
138+
}
127139
}
128140
else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
129141
builder = OAuth2AuthorizationRequest.implicit();
@@ -164,4 +176,34 @@ private String expandRedirectUri(ServerHttpRequest request, ClientRegistration c
164176
.buildAndExpand(uriVariables)
165177
.toUriString();
166178
}
179+
180+
/**
181+
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
182+
*
183+
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
184+
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
185+
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
186+
*
187+
* @since 5.2
188+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
189+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
190+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
191+
*/
192+
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
193+
String codeVerifier = this.codeVerifierGenerator.generateKey();
194+
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
195+
try {
196+
String codeChallenge = createCodeChallenge(codeVerifier);
197+
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
198+
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
199+
} catch (NoSuchAlgorithmException e) {
200+
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
201+
}
202+
}
203+
204+
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
205+
MessageDigest md = MessageDigest.getInstance("SHA-256");
206+
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
207+
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
208+
}
167209
}

0 commit comments

Comments
 (0)