Skip to content

Commit a2ffc88

Browse files
committed
Allow configuring PKCE for confidential clients
Closes gh-6548
1 parent 7955e5a commit a2ffc88

File tree

8 files changed

+369
-147
lines changed

8 files changed

+369
-147
lines changed

docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ If the client is running in an untrusted environment (eg. native application or
7272
. `client-secret` is omitted (or empty)
7373
. `client-authentication-method` is set to "none" (`ClientAuthenticationMethod.NONE`)
7474

75+
[TIP]
76+
If the OAuth 2.0 Provider supports PKCE for https://tools.ietf.org/html/rfc6749#section-2.1[Confidential Clients], you may (optionally) configure it using `DefaultServerOAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce())`.
77+
7578
[[oauth2Client-auth-code-redirect-uri]]
7679
The `DefaultServerOAuth2AuthorizationRequestResolver` also supports `URI` template variables for the `redirect-uri` using `UriComponentsBuilder`.
7780

docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ If the client is running in an untrusted environment (eg. native application or
7272
. `client-secret` is omitted (or empty)
7373
. `client-authentication-method` is set to "none" (`ClientAuthenticationMethod.NONE`)
7474

75+
[TIP]
76+
If the OAuth 2.0 Provider supports PKCE for https://tools.ietf.org/html/rfc6749#section-2.1[Confidential Clients], you may (optionally) configure it using `DefaultOAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce())`.
77+
7578
[[oauth2Client-auth-code-redirect-uri]]
7679
The `DefaultOAuth2AuthorizationRequestResolver` also supports `URI` template variables for the `redirect-uri` using `UriComponentsBuilder`.
7780

docs/modules/ROOT/pages/whats-new.adoc

+18-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,21 @@
44
Spring Security 5.7 provides a number of new features.
55
Below are the highlights of the release.
66

7-
* xref:servlet/authentication/persistence.adoc#requestattributesecuritycontextrepository[`RequestAttributeSecurityContextRepository`]
8-
* xref:servlet/authentication/persistence.adoc#securitycontextholderfilter[`SecurityContextHolderFilter`] - Ability to require explicit saving of the `SecurityContext`.
7+
[[whats-new-servlet]]
8+
== Servlet
9+
10+
* Web
11+
12+
** Introduced xref:servlet/authentication/persistence.adoc#requestattributesecuritycontextrepository[`RequestAttributeSecurityContextRepository`]
13+
** Introduced xref:servlet/authentication/persistence.adoc#securitycontextholderfilter[`SecurityContextHolderFilter`] - Ability to require explicit saving of the `SecurityContext`
14+
15+
* OAuth 2.0 Client
16+
17+
** Allow configuring https://github.com/spring-projects/spring-security/issues/6548[PKCE for confidential clients]
18+
19+
[[whats-new-webflux]]
20+
== WebFlux
21+
22+
* OAuth 2.0 Client
23+
24+
** Allow configuring https://github.com/spring-projects/spring-security/issues/6548[PKCE for confidential clients]

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

+28-58
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 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.
@@ -34,7 +34,6 @@
3434
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
3535
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
3636
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
37-
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
3837
import org.springframework.security.oauth2.core.oidc.OidcScopes;
3938
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
4039
import org.springframework.security.web.util.UrlUtils;
@@ -70,14 +69,18 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
7069

7170
private static final char PATH_DELIMITER = '/';
7271

73-
private final ClientRegistrationRepository clientRegistrationRepository;
72+
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
73+
Base64.getUrlEncoder());
7474

75-
private final AntPathRequestMatcher authorizationRequestMatcher;
75+
private static final StringKeyGenerator DEFAULT_SECURE_KEY_GENERATOR = new Base64StringKeyGenerator(
76+
Base64.getUrlEncoder().withoutPadding(), 96);
7677

77-
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
78+
private static final Consumer<OAuth2AuthorizationRequest.Builder> DEFAULT_PKCE_APPLIER = OAuth2AuthorizationRequestCustomizers
79+
.withPkce();
7880

79-
private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(
80-
Base64.getUrlEncoder().withoutPadding(), 96);
81+
private final ClientRegistrationRepository clientRegistrationRepository;
82+
83+
private final AntPathRequestMatcher authorizationRequestMatcher;
8184

8285
private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer = (customizer) -> {
8386
};
@@ -100,7 +103,7 @@ public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository cl
100103

101104
@Override
102105
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
103-
String registrationId = this.resolveRegistrationId(request);
106+
String registrationId = resolveRegistrationId(request);
104107
if (registrationId == null) {
105108
return null;
106109
}
@@ -123,6 +126,7 @@ public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String reg
123126
* @param authorizationRequestCustomizer the {@code Consumer} to be provided the
124127
* {@link OAuth2AuthorizationRequest.Builder}
125128
* @since 5.3
129+
* @see OAuth2AuthorizationRequestCustomizers
126130
*/
127131
public void setAuthorizationRequestCustomizer(
128132
Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer) {
@@ -147,9 +151,7 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re
147151
if (clientRegistration == null) {
148152
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
149153
}
150-
Map<String, Object> attributes = new HashMap<>();
151-
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
152-
OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration, attributes);
154+
OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration);
153155

154156
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
155157

@@ -158,32 +160,32 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re
158160
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
159161
.redirectUri(redirectUriStr)
160162
.scopes(clientRegistration.getScopes())
161-
.state(this.stateGenerator.generateKey())
162-
.attributes(attributes);
163+
.state(DEFAULT_STATE_GENERATOR.generateKey());
163164
// @formatter:on
164165

165166
this.authorizationRequestCustomizer.accept(builder);
166167

167168
return builder.build();
168169
}
169170

170-
private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientRegistration,
171-
Map<String, Object> attributes) {
171+
private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientRegistration) {
172172
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
173-
OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode();
174-
Map<String, Object> additionalParameters = new HashMap<>();
173+
// @formatter:off
174+
OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode()
175+
.attributes((attrs) ->
176+
attrs.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
177+
// @formatter:on
175178
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())
176179
&& clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
177180
// Section 3.1.2.1 Authentication Request -
178181
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope
179182
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope
180183
// value.
181-
addNonceParameters(attributes, additionalParameters);
184+
applyNonce(builder);
182185
}
183186
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
184-
addPkceParameters(attributes, additionalParameters);
187+
DEFAULT_PKCE_APPLIER.accept(builder);
185188
}
186-
builder.additionalParameters(additionalParameters);
187189
return builder;
188190
}
189191
if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
@@ -252,54 +254,22 @@ private static String expandRedirectUri(HttpServletRequest request, ClientRegist
252254

253255
/**
254256
* Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests.
255-
* @param attributes where the {@link OidcParameterNames#NONCE} is stored for the
256-
* authentication request
257-
* @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is
258-
* added for the authentication request
257+
* @param builder where the {@link OidcParameterNames#NONCE} and hash is stored for
258+
* the authentication request
259259
*
260260
* @since 5.2
261261
* @see <a target="_blank" href=
262262
* "https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest">3.1.2.1.
263263
* Authentication Request</a>
264264
*/
265-
private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
265+
private static void applyNonce(OAuth2AuthorizationRequest.Builder builder) {
266266
try {
267-
String nonce = this.secureKeyGenerator.generateKey();
267+
String nonce = DEFAULT_SECURE_KEY_GENERATOR.generateKey();
268268
String nonceHash = createHash(nonce);
269-
attributes.put(OidcParameterNames.NONCE, nonce);
270-
additionalParameters.put(OidcParameterNames.NONCE, nonceHash);
271-
}
272-
catch (NoSuchAlgorithmException ex) {
273-
}
274-
}
275-
276-
/**
277-
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization
278-
* and Access Token Requests
279-
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the
280-
* token request
281-
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and,
282-
* usually, {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in
283-
* the authorization request.
284-
*
285-
* @since 5.2
286-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1.
287-
* Protocol Flow</a>
288-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1.
289-
* Client Creates a Code Verifier</a>
290-
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2.
291-
* Client Creates the Code Challenge</a>
292-
*/
293-
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
294-
String codeVerifier = this.secureKeyGenerator.generateKey();
295-
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
296-
try {
297-
String codeChallenge = createHash(codeVerifier);
298-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
299-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
269+
builder.attributes((attrs) -> attrs.put(OidcParameterNames.NONCE, nonce));
270+
builder.additionalParameters((params) -> params.put(OidcParameterNames.NONCE, nonceHash));
300271
}
301272
catch (NoSuchAlgorithmException ex) {
302-
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
303273
}
304274
}
305275

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2002-2022 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+
17+
package org.springframework.security.oauth2.client.web;
18+
19+
import java.nio.charset.StandardCharsets;
20+
import java.security.MessageDigest;
21+
import java.security.NoSuchAlgorithmException;
22+
import java.util.Base64;
23+
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.function.Consumer;
25+
26+
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
27+
import org.springframework.security.crypto.keygen.StringKeyGenerator;
28+
import org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizationRequestResolver;
29+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
30+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
31+
32+
/**
33+
* A factory of customizers that customize the {@link OAuth2AuthorizationRequest OAuth 2.0
34+
* Authorization Request} via the {@link OAuth2AuthorizationRequest.Builder}.
35+
*
36+
* @author Joe Grandja
37+
* @since 5.7
38+
* @see OAuth2AuthorizationRequest.Builder
39+
* @see DefaultOAuth2AuthorizationRequestResolver#setAuthorizationRequestCustomizer(Consumer)
40+
* @see DefaultServerOAuth2AuthorizationRequestResolver#setAuthorizationRequestCustomizer(Consumer)
41+
*/
42+
public final class OAuth2AuthorizationRequestCustomizers {
43+
44+
private static final StringKeyGenerator DEFAULT_SECURE_KEY_GENERATOR = new Base64StringKeyGenerator(
45+
Base64.getUrlEncoder().withoutPadding(), 96);
46+
47+
private OAuth2AuthorizationRequestCustomizers() {
48+
}
49+
50+
/**
51+
* Returns a {@code Consumer} to be provided the
52+
* {@link OAuth2AuthorizationRequest.Builder} that adds the
53+
* {@link PkceParameterNames#CODE_CHALLENGE code_challenge} and, usually,
54+
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD code_challenge_method} parameters
55+
* to the OAuth 2.0 Authorization Request. The {@code code_verifier} is stored in
56+
* {@link OAuth2AuthorizationRequest#getAttribute(String)} under the key
57+
* {@link PkceParameterNames#CODE_VERIFIER code_verifier} for subsequent use in the
58+
* OAuth 2.0 Access Token Request.
59+
* @return a {@code Consumer} to be provided the
60+
* {@link OAuth2AuthorizationRequest.Builder} that adds the PKCE parameters
61+
* @see <a target="_blank" href=
62+
* "https://datatracker.ietf.org/doc/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
63+
* @see <a target="_blank" href=
64+
* "https://datatracker.ietf.org/doc/html/rfc7636#section-4.1">4.1. Client Creates a
65+
* Code Verifier</a>
66+
* @see <a target="_blank" href=
67+
* "https://datatracker.ietf.org/doc/html/rfc7636#section-4.2">4.2. Client Creates the
68+
* Code Challenge</a>
69+
*/
70+
public static Consumer<OAuth2AuthorizationRequest.Builder> withPkce() {
71+
return OAuth2AuthorizationRequestCustomizers::applyPkce;
72+
}
73+
74+
private static void applyPkce(OAuth2AuthorizationRequest.Builder builder) {
75+
if (isPkceAlreadyApplied(builder)) {
76+
return;
77+
}
78+
79+
String codeVerifier = DEFAULT_SECURE_KEY_GENERATOR.generateKey();
80+
81+
builder.attributes((attrs) -> attrs.put(PkceParameterNames.CODE_VERIFIER, codeVerifier));
82+
83+
builder.additionalParameters((params) -> {
84+
try {
85+
String codeChallenge = createHash(codeVerifier);
86+
params.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
87+
params.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
88+
}
89+
catch (NoSuchAlgorithmException ex) {
90+
params.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
91+
}
92+
});
93+
}
94+
95+
private static boolean isPkceAlreadyApplied(OAuth2AuthorizationRequest.Builder builder) {
96+
AtomicBoolean pkceApplied = new AtomicBoolean(false);
97+
builder.additionalParameters((params) -> {
98+
if (params.containsKey(PkceParameterNames.CODE_CHALLENGE)) {
99+
pkceApplied.set(true);
100+
}
101+
});
102+
return pkceApplied.get();
103+
}
104+
105+
private static String createHash(String value) throws NoSuchAlgorithmException {
106+
MessageDigest md = MessageDigest.getInstance("SHA-256");
107+
byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
108+
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
109+
}
110+
111+
}

0 commit comments

Comments
 (0)