Skip to content

Commit 8d3e084

Browse files
kse-musicrwinch
authored andcommitted
Add ClientRegistration.clientSettings.requireProofKey to Enable PKCE
Closes gh-16382 Signed-off-by: DingHao <[email protected]>
1 parent 8acd1d3 commit 8d3e084

File tree

6 files changed

+139
-9
lines changed

6 files changed

+139
-9
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java

+1-1
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-2025 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.

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

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -71,6 +71,8 @@ public final class ClientRegistration implements Serializable {
7171

7272
private String clientName;
7373

74+
private ClientSettings clientSettings;
75+
7476
private ClientRegistration() {
7577
}
7678

@@ -162,6 +164,14 @@ public String getClientName() {
162164
return this.clientName;
163165
}
164166

167+
/**
168+
* Returns the {@link ClientSettings client configuration settings}.
169+
* @return the {@link ClientSettings}
170+
*/
171+
public ClientSettings getClientSettings() {
172+
return this.clientSettings;
173+
}
174+
165175
@Override
166176
public String toString() {
167177
// @formatter:off
@@ -175,6 +185,7 @@ public String toString() {
175185
+ '\'' + ", scopes=" + this.scopes
176186
+ ", providerDetails=" + this.providerDetails
177187
+ ", clientName='" + this.clientName + '\''
188+
+ ", clientSettings='" + this.clientSettings + '\''
178189
+ '}';
179190
// @formatter:on
180191
}
@@ -367,6 +378,8 @@ public static final class Builder implements Serializable {
367378

368379
private String clientName;
369380

381+
private ClientSettings clientSettings;
382+
370383
private Builder(String registrationId) {
371384
this.registrationId = registrationId;
372385
}
@@ -391,6 +404,7 @@ private Builder(ClientRegistration clientRegistration) {
391404
this.configurationMetadata = new HashMap<>(configurationMetadata);
392405
}
393406
this.clientName = clientRegistration.clientName;
407+
this.clientSettings = clientRegistration.clientSettings;
394408
}
395409

396410
/**
@@ -594,6 +608,16 @@ public Builder clientName(String clientName) {
594608
return this;
595609
}
596610

611+
/**
612+
* Sets the {@link ClientSettings client configuration settings}.
613+
* @param clientSettings the client configuration settings
614+
* @return the {@link Builder}
615+
*/
616+
public Builder clientSettings(ClientSettings clientSettings) {
617+
this.clientSettings = clientSettings;
618+
return this;
619+
}
620+
597621
/**
598622
* Builds a new {@link ClientRegistration}.
599623
* @return a {@link ClientRegistration}
@@ -627,12 +651,14 @@ private ClientRegistration create() {
627651
clientRegistration.providerDetails = createProviderDetails(clientRegistration);
628652
clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName
629653
: this.registrationId;
654+
clientRegistration.clientSettings = (this.clientSettings == null) ? ClientSettings.builder().build()
655+
: this.clientSettings;
630656
return clientRegistration;
631657
}
632658

633659
private ClientAuthenticationMethod deduceClientAuthenticationMethod(ClientRegistration clientRegistration) {
634660
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)
635-
&& !StringUtils.hasText(this.clientSecret)) {
661+
&& (!StringUtils.hasText(this.clientSecret))) {
636662
return ClientAuthenticationMethod.NONE;
637663
}
638664
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2002-2025 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.registration;
18+
19+
/**
20+
* A facility for client configuration settings.
21+
*
22+
* @author DingHao
23+
* @since 6.5
24+
*/
25+
public final class ClientSettings {
26+
27+
private boolean requireProofKey;
28+
29+
private ClientSettings() {
30+
31+
}
32+
33+
public boolean isRequireProofKey() {
34+
return this.requireProofKey;
35+
}
36+
37+
public static Builder builder() {
38+
return new Builder();
39+
}
40+
41+
public static final class Builder {
42+
43+
private boolean requireProofKey;
44+
45+
private Builder() {
46+
}
47+
48+
/**
49+
* Set to {@code true} if the client is required to provide a proof key challenge
50+
* and verifier when performing the Authorization Code Grant flow.
51+
* @param requireProofKey {@code true} if the client is required to provide a
52+
* proof key challenge and verifier, {@code false} otherwise
53+
* @return the {@link Builder} for further configuration
54+
*/
55+
public Builder requireProofKey(boolean requireProofKey) {
56+
this.requireProofKey = requireProofKey;
57+
return this;
58+
}
59+
60+
public ClientSettings build() {
61+
ClientSettings clientSettings = new ClientSettings();
62+
clientSettings.requireProofKey = this.requireProofKey;
63+
return clientSettings;
64+
}
65+
66+
}
67+
68+
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -183,7 +183,8 @@ private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientR
183183
// value.
184184
applyNonce(builder);
185185
}
186-
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
186+
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())
187+
|| clientRegistration.getClientSettings().isRequireProofKey()) {
187188
DEFAULT_PKCE_APPLIER.accept(builder);
188189
}
189190
return builder;

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,10 @@ private static String asJson(ClientRegistration clientRegistration) {
276276
" " + configurationMetadata + "\n" +
277277
" }\n" +
278278
" },\n" +
279-
" \"clientName\": \"" + clientRegistration.getClientName() + "\"\n" +
279+
" \"clientName\": \"" + clientRegistration.getClientName() + "\",\n" +
280+
" \"clientSettings\": {\n" +
281+
" \"requireProofKey\": " + clientRegistration.getClientSettings().isRequireProofKey() + "\n" +
282+
" }\n" +
280283
"}";
281284
// @formatter:on
282285
}

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

+35-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -28,6 +28,7 @@
2828
import org.springframework.mock.web.MockHttpServletRequest;
2929
import org.springframework.security.oauth2.client.registration.ClientRegistration;
3030
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
31+
import org.springframework.security.oauth2.client.registration.ClientSettings;
3132
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
3233
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
3334
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -56,6 +57,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
5657

5758
private ClientRegistration registration2;
5859

60+
private ClientRegistration pkceClientRegistration;
61+
5962
private ClientRegistration fineRedirectUriTemplateRegistration;
6063

6164
private ClientRegistration publicClientRegistration;
@@ -72,6 +75,9 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
7275
public void setUp() {
7376
this.registration1 = TestClientRegistrations.clientRegistration().build();
7477
this.registration2 = TestClientRegistrations.clientRegistration2().build();
78+
79+
this.pkceClientRegistration = pkceClientRegistration().build();
80+
7581
this.fineRedirectUriTemplateRegistration = fineRedirectUriTemplateClientRegistration().build();
7682
// @formatter:off
7783
this.publicClientRegistration = TestClientRegistrations.clientRegistration()
@@ -86,8 +92,8 @@ public void setUp() {
8692
.build();
8793
// @formatter:on
8894
this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1,
89-
this.registration2, this.fineRedirectUriTemplateRegistration, this.publicClientRegistration,
90-
this.oidcRegistration);
95+
this.registration2, this.pkceClientRegistration, this.fineRedirectUriTemplateRegistration,
96+
this.publicClientRegistration, this.oidcRegistration);
9197
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository,
9298
this.authorizationRequestBaseUri);
9399
}
@@ -563,6 +569,32 @@ public void resolveWhenAuthorizationRequestCustomizerOverridesParameterThenQuery
563569
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "appid=client-id");
564570
}
565571

572+
@Test
573+
public void resolveWhenAuthorizationRequestProvideCodeChallengeMethod() {
574+
ClientRegistration clientRegistration = this.pkceClientRegistration;
575+
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
576+
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
577+
request.setServletPath(requestUri);
578+
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
579+
assertThat(authorizationRequest.getAdditionalParameters().containsKey(PkceParameterNames.CODE_CHALLENGE_METHOD))
580+
.isTrue();
581+
}
582+
583+
private static ClientRegistration.Builder pkceClientRegistration() {
584+
return ClientRegistration.withRegistrationId("pkce")
585+
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
586+
.clientSettings(ClientSettings.builder().requireProofKey(true).build())
587+
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
588+
.scope("read:user")
589+
.authorizationUri("https://example.com/login/oauth/authorize")
590+
.tokenUri("https://example.com/login/oauth/access_token")
591+
.userInfoUri("https://api.example.com/user")
592+
.userNameAttributeName("id")
593+
.clientName("Client Name")
594+
.clientId("client-id-3")
595+
.clientSecret("client-secret");
596+
}
597+
566598
private static ClientRegistration.Builder fineRedirectUriTemplateClientRegistration() {
567599
// @formatter:off
568600
return ClientRegistration.withRegistrationId("fine-redirect-uri-template-client-registration")

0 commit comments

Comments
 (0)