Skip to content

Commit 40b8f26

Browse files
committed
Allow PKCE to be configured for confidential clients
Fixes gh-6548
1 parent 4ca9e15 commit 40b8f26

File tree

8 files changed

+302
-111
lines changed

8 files changed

+302
-111
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.client.endpoint;
17+
18+
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
19+
import org.springframework.security.crypto.keygen.StringKeyGenerator;
20+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
21+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
22+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
23+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
24+
25+
import java.nio.charset.StandardCharsets;
26+
import java.security.MessageDigest;
27+
import java.security.NoSuchAlgorithmException;
28+
import java.util.Base64;
29+
import java.util.function.BiConsumer;
30+
import java.util.function.Consumer;
31+
32+
/**
33+
* A {@link Consumer} of {@link OAuth2AuthorizationRequest.Builder} that
34+
* adds additional {@link PkceParameterNames PKCE parameters}
35+
* for use in the OAuth 2.0 Authorization Request and Access Token Request.
36+
*
37+
* <p>
38+
* The {@link PkceParameterNames#CODE_CHALLENGE} and {@link PkceParameterNames#CODE_CHALLENGE_METHOD}
39+
* are added as {@link OAuth2AuthorizationRequest#getAdditionalParameters() additional parameters} in the Authorization Request.
40+
* The {@link PkceParameterNames#CODE_VERIFIER} is stored as an {@link OAuth2AuthorizationRequest#getAttributes() attribute}
41+
* for use in the Access Token Request.
42+
*
43+
* @author Stephen Doxsee
44+
* @author Kevin Bolduc
45+
* @author Joe Grandja
46+
* @since 5.2
47+
* @see OAuth2AuthorizationRequest.Builder
48+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">Section 1.1 Protocol Flow</a>
49+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">Section 4.1 Client Creates a Code Verifier</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>
51+
*/
52+
public final class PkceParameterBuilder implements BiConsumer<OAuth2AuthorizationRequest.Builder, ClientRegistration> {
53+
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
54+
55+
@Override
56+
public void accept(OAuth2AuthorizationRequest.Builder builder, ClientRegistration clientRegistration) {
57+
if (!AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
58+
return;
59+
}
60+
61+
String codeVerifier = this.codeVerifierGenerator.generateKey();
62+
builder.attributes(attrs -> attrs.put(PkceParameterNames.CODE_VERIFIER, codeVerifier));
63+
try {
64+
String codeChallenge = createCodeChallenge(codeVerifier);
65+
builder.additionalParameters(params -> {
66+
params.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
67+
params.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
68+
});
69+
} catch (NoSuchAlgorithmException ex) {
70+
builder.additionalParameters(params -> params.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier));
71+
}
72+
}
73+
74+
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
75+
MessageDigest md = MessageDigest.getInstance("SHA-256");
76+
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
77+
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
78+
}
79+
}

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

+31-47
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717

1818
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
1919
import org.springframework.security.crypto.keygen.StringKeyGenerator;
20+
import org.springframework.security.oauth2.client.endpoint.PkceParameterBuilder;
2021
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2122
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
2223
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2324
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2425
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
2526
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
26-
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
2727
import org.springframework.security.web.util.UrlUtils;
2828
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
2929
import org.springframework.util.Assert;
@@ -32,12 +32,10 @@
3232
import org.springframework.web.util.UriComponentsBuilder;
3333

3434
import javax.servlet.http.HttpServletRequest;
35-
import java.nio.charset.StandardCharsets;
36-
import java.security.MessageDigest;
37-
import java.security.NoSuchAlgorithmException;
3835
import java.util.Base64;
3936
import java.util.HashMap;
4037
import java.util.Map;
38+
import java.util.function.BiConsumer;
4139

4240
/**
4341
* An implementation of an {@link OAuth2AuthorizationRequestResolver} that attempts to
@@ -53,14 +51,17 @@
5351
* @since 5.1
5452
* @see OAuth2AuthorizationRequestResolver
5553
* @see OAuth2AuthorizationRequestRedirectFilter
54+
* @see OAuth2AuthorizationRequest
55+
* @see PkceParameterBuilder
5656
*/
5757
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
5858
private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
5959
private static final char PATH_DELIMITER = '/';
6060
private final ClientRegistrationRepository clientRegistrationRepository;
6161
private final AntPathRequestMatcher authorizationRequestMatcher;
6262
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
63-
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
63+
private final BiConsumer<OAuth2AuthorizationRequest.Builder, ClientRegistration> pkceParameterBuilder = new PkceParameterBuilder();
64+
private BiConsumer<OAuth2AuthorizationRequest.Builder, ClientRegistration> authorizationRequestBuilder;
6465

6566
/**
6667
* Constructs a {@code DefaultOAuth2AuthorizationRequestResolver} using the provided parameters.
@@ -93,6 +94,18 @@ public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String reg
9394
return resolve(request, registrationId, redirectUriAction);
9495
}
9596

97+
/**
98+
* Sets the {@link BiConsumer} that is ultimately supplied with the {@link OAuth2AuthorizationRequest.Builder} instance.
99+
* This provides the ability for the {@code BiConsumer} to mutate the {@link OAuth2AuthorizationRequest} before it is built.
100+
*
101+
* @since 5.2
102+
* @param authorizationRequestBuilder the {@link BiConsumer} that is supplied the {@code OAuth2AuthorizationRequest.Builder} instance
103+
*/
104+
public void setAuthorizationRequestBuilder(BiConsumer<OAuth2AuthorizationRequest.Builder, ClientRegistration> authorizationRequestBuilder) {
105+
Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null");
106+
this.authorizationRequestBuilder = authorizationRequestBuilder;
107+
}
108+
96109
private String getAction(HttpServletRequest request, String defaultAction) {
97110
String action = request.getParameter("action");
98111
if (action == null) {
@@ -111,17 +124,9 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re
111124
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
112125
}
113126

114-
Map<String, Object> attributes = new HashMap<>();
115-
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
116-
117127
OAuth2AuthorizationRequest.Builder builder;
118128
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
119129
builder = OAuth2AuthorizationRequest.authorizationCode();
120-
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
121-
Map<String, Object> additionalParameters = new HashMap<>();
122-
addPkceParameters(attributes, additionalParameters);
123-
builder.additionalParameters(additionalParameters);
124-
}
125130
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
126131
builder = OAuth2AuthorizationRequest.implicit();
127132
} else {
@@ -132,16 +137,25 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re
132137

133138
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
134139

135-
OAuth2AuthorizationRequest authorizationRequest = builder
140+
builder
136141
.clientId(clientRegistration.getClientId())
137142
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
138143
.redirectUri(redirectUriStr)
139144
.scopes(clientRegistration.getScopes())
140145
.state(this.stateGenerator.generateKey())
141-
.attributes(attributes)
142-
.build();
146+
.attributes(attrs -> attrs.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
143147

144-
return authorizationRequest;
148+
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType()) &&
149+
ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
150+
// Add PKCE parameters for public clients
151+
this.pkceParameterBuilder.accept(builder, clientRegistration);
152+
}
153+
154+
if (this.authorizationRequestBuilder != null) {
155+
this.authorizationRequestBuilder.accept(builder, clientRegistration);
156+
}
157+
158+
return builder.build();
145159
}
146160

147161
private String resolveRegistrationId(HttpServletRequest request) {
@@ -199,34 +213,4 @@ private static String expandRedirectUri(HttpServletRequest request, ClientRegist
199213
.buildAndExpand(uriVariables)
200214
.toUriString();
201215
}
202-
203-
/**
204-
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
205-
*
206-
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
207-
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
208-
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
209-
*
210-
* @since 5.2
211-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
212-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
213-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
214-
*/
215-
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
216-
String codeVerifier = this.codeVerifierGenerator.generateKey();
217-
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
218-
try {
219-
String codeChallenge = createCodeChallenge(codeVerifier);
220-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
221-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
222-
} catch (NoSuchAlgorithmException e) {
223-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
224-
}
225-
}
226-
227-
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
228-
MessageDigest md = MessageDigest.getInstance("SHA-256");
229-
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
230-
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
231-
}
232216
}

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

+40-48
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
import org.springframework.http.server.reactive.ServerHttpRequest;
2121
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
2222
import org.springframework.security.crypto.keygen.StringKeyGenerator;
23+
import org.springframework.security.oauth2.client.endpoint.PkceParameterBuilder;
2324
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2425
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
2526
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2627
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;
29-
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
3030
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
3131
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
3232
import org.springframework.util.Assert;
@@ -37,12 +37,10 @@
3737
import org.springframework.web.util.UriComponentsBuilder;
3838
import reactor.core.publisher.Mono;
3939

40-
import java.nio.charset.StandardCharsets;
41-
import java.security.MessageDigest;
42-
import java.security.NoSuchAlgorithmException;
4340
import java.util.Base64;
4441
import java.util.HashMap;
4542
import java.util.Map;
43+
import java.util.function.BiConsumer;
4644

4745
/**
4846
* The default implementation of {@link ServerOAuth2AuthorizationRequestResolver}.
@@ -53,6 +51,10 @@
5351
*
5452
* @author Rob Winch
5553
* @since 5.1
54+
* @see ServerOAuth2AuthorizationRequestResolver
55+
* @see OAuth2AuthorizationRequestRedirectWebFilter
56+
* @see OAuth2AuthorizationRequest
57+
* @see PkceParameterBuilder
5658
*/
5759
public class DefaultServerOAuth2AuthorizationRequestResolver
5860
implements ServerOAuth2AuthorizationRequestResolver {
@@ -75,7 +77,9 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
7577

7678
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
7779

78-
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
80+
private final BiConsumer<OAuth2AuthorizationRequest.Builder, ClientRegistration> pkceParameterBuilder = new PkceParameterBuilder();
81+
82+
private BiConsumer<OAuth2AuthorizationRequest.Builder, ClientRegistration> authorizationRequestBuilder;
7983

8084
/**
8185
* Creates a new instance
@@ -117,26 +121,29 @@ public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange,
117121
.map(clientRegistration -> authorizationRequest(exchange, clientRegistration));
118122
}
119123

124+
/**
125+
* Sets the {@link BiConsumer} that is ultimately supplied with the {@link OAuth2AuthorizationRequest.Builder} instance.
126+
* This provides the ability for the {@code BiConsumer} to mutate the {@link OAuth2AuthorizationRequest} before it is built.
127+
*
128+
* @since 5.2
129+
* @param authorizationRequestBuilder the {@link BiConsumer} that is supplied the {@code OAuth2AuthorizationRequest.Builder} instance
130+
*/
131+
public void setAuthorizationRequestBuilder(BiConsumer<OAuth2AuthorizationRequest.Builder, ClientRegistration> authorizationRequestBuilder) {
132+
Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null");
133+
this.authorizationRequestBuilder = authorizationRequestBuilder;
134+
}
135+
120136
private Mono<ClientRegistration> findByRegistrationId(ServerWebExchange exchange, String clientRegistration) {
121137
return this.clientRegistrationRepository.findByRegistrationId(clientRegistration)
122138
.switchIfEmpty(Mono.error(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid client registration id")));
123139
}
124140

125141
private OAuth2AuthorizationRequest authorizationRequest(ServerWebExchange exchange,
126142
ClientRegistration clientRegistration) {
127-
String redirectUriStr = expandRedirectUri(exchange.getRequest(), clientRegistration);
128-
129-
Map<String, Object> attributes = new HashMap<>();
130-
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
131143

132144
OAuth2AuthorizationRequest.Builder builder;
133145
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
134146
builder = OAuth2AuthorizationRequest.authorizationCode();
135-
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
136-
Map<String, Object> additionalParameters = new HashMap<>();
137-
addPkceParameters(attributes, additionalParameters);
138-
builder.additionalParameters(additionalParameters);
139-
}
140147
}
141148
else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
142149
builder = OAuth2AuthorizationRequest.implicit();
@@ -146,13 +153,28 @@ else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizat
146153
"Invalid Authorization Grant Type (" + clientRegistration.getAuthorizationGrantType().getValue()
147154
+ ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
148155
}
149-
return builder
156+
157+
String redirectUriStr = expandRedirectUri(exchange.getRequest(), clientRegistration);
158+
159+
builder
150160
.clientId(clientRegistration.getClientId())
151161
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
152-
.redirectUri(redirectUriStr).scopes(clientRegistration.getScopes())
162+
.redirectUri(redirectUriStr)
163+
.scopes(clientRegistration.getScopes())
153164
.state(this.stateGenerator.generateKey())
154-
.attributes(attributes)
155-
.build();
165+
.attributes(attrs -> attrs.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
166+
167+
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType()) &&
168+
ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
169+
// Add PKCE parameters for public clients
170+
this.pkceParameterBuilder.accept(builder, clientRegistration);
171+
}
172+
173+
if (this.authorizationRequestBuilder != null) {
174+
this.authorizationRequestBuilder.accept(builder, clientRegistration);
175+
}
176+
177+
return builder.build();
156178
}
157179

158180
/**
@@ -206,34 +228,4 @@ private static String expandRedirectUri(ServerHttpRequest request, ClientRegistr
206228
.buildAndExpand(uriVariables)
207229
.toUriString();
208230
}
209-
210-
/**
211-
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
212-
*
213-
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
214-
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
215-
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
216-
*
217-
* @since 5.2
218-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
219-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
220-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
221-
*/
222-
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
223-
String codeVerifier = this.codeVerifierGenerator.generateKey();
224-
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
225-
try {
226-
String codeChallenge = createCodeChallenge(codeVerifier);
227-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
228-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
229-
} catch (NoSuchAlgorithmException e) {
230-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
231-
}
232-
}
233-
234-
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
235-
MessageDigest md = MessageDigest.getInstance("SHA-256");
236-
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
237-
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
238-
}
239231
}

0 commit comments

Comments
 (0)