Add ClientRegistration.clientSettings.requireProofKey to Enable PKCE

Closes gh-16382

Signed-off-by: DingHao <dh.hiekn@gmail.com>
This commit is contained in:
DingHao 2025-01-09 17:32:25 +08:00 committed by Rob Winch
parent 8acd1d3f51
commit 8d3e0844c5
No known key found for this signature in database
6 changed files with 139 additions and 9 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 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.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@ -71,6 +71,8 @@ public final class ClientRegistration implements Serializable {
private String clientName;
private ClientSettings clientSettings;
private ClientRegistration() {
}
@ -162,6 +164,14 @@ public final class ClientRegistration implements Serializable {
return this.clientName;
}
/**
* Returns the {@link ClientSettings client configuration settings}.
* @return the {@link ClientSettings}
*/
public ClientSettings getClientSettings() {
return this.clientSettings;
}
@Override
public String toString() {
// @formatter:off
@ -175,6 +185,7 @@ public final class ClientRegistration implements Serializable {
+ '\'' + ", scopes=" + this.scopes
+ ", providerDetails=" + this.providerDetails
+ ", clientName='" + this.clientName + '\''
+ ", clientSettings='" + this.clientSettings + '\''
+ '}';
// @formatter:on
}
@ -367,6 +378,8 @@ public final class ClientRegistration implements Serializable {
private String clientName;
private ClientSettings clientSettings;
private Builder(String registrationId) {
this.registrationId = registrationId;
}
@ -391,6 +404,7 @@ public final class ClientRegistration implements Serializable {
this.configurationMetadata = new HashMap<>(configurationMetadata);
}
this.clientName = clientRegistration.clientName;
this.clientSettings = clientRegistration.clientSettings;
}
/**
@ -594,6 +608,16 @@ public final class ClientRegistration implements Serializable {
return this;
}
/**
* Sets the {@link ClientSettings client configuration settings}.
* @param clientSettings the client configuration settings
* @return the {@link Builder}
*/
public Builder clientSettings(ClientSettings clientSettings) {
this.clientSettings = clientSettings;
return this;
}
/**
* Builds a new {@link ClientRegistration}.
* @return a {@link ClientRegistration}
@ -627,12 +651,14 @@ public final class ClientRegistration implements Serializable {
clientRegistration.providerDetails = createProviderDetails(clientRegistration);
clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName
: this.registrationId;
clientRegistration.clientSettings = (this.clientSettings == null) ? ClientSettings.builder().build()
: this.clientSettings;
return clientRegistration;
}
private ClientAuthenticationMethod deduceClientAuthenticationMethod(ClientRegistration clientRegistration) {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)
&& !StringUtils.hasText(this.clientSecret)) {
&& (!StringUtils.hasText(this.clientSecret))) {
return ClientAuthenticationMethod.NONE;
}
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;

View File

@ -0,0 +1,68 @@
/*
* Copyright 2002-2025 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
*
* https://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.client.registration;
/**
* A facility for client configuration settings.
*
* @author DingHao
* @since 6.5
*/
public final class ClientSettings {
private boolean requireProofKey;
private ClientSettings() {
}
public boolean isRequireProofKey() {
return this.requireProofKey;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private boolean requireProofKey;
private Builder() {
}
/**
* Set to {@code true} if the client is required to provide a proof key challenge
* and verifier when performing the Authorization Code Grant flow.
* @param requireProofKey {@code true} if the client is required to provide a
* proof key challenge and verifier, {@code false} otherwise
* @return the {@link Builder} for further configuration
*/
public Builder requireProofKey(boolean requireProofKey) {
this.requireProofKey = requireProofKey;
return this;
}
public ClientSettings build() {
ClientSettings clientSettings = new ClientSettings();
clientSettings.requireProofKey = this.requireProofKey;
return clientSettings;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@ -183,7 +183,8 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
// value.
applyNonce(builder);
}
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())
|| clientRegistration.getClientSettings().isRequireProofKey()) {
DEFAULT_PKCE_APPLIER.accept(builder);
}
return builder;

View File

@ -276,7 +276,10 @@ public class OAuth2AuthorizedClientMixinTests {
" " + configurationMetadata + "\n" +
" }\n" +
" },\n" +
" \"clientName\": \"" + clientRegistration.getClientName() + "\"\n" +
" \"clientName\": \"" + clientRegistration.getClientName() + "\",\n" +
" \"clientSettings\": {\n" +
" \"requireProofKey\": " + clientRegistration.getClientSettings().isRequireProofKey() + "\n" +
" }\n" +
"}";
// @formatter:on
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@ -28,6 +28,7 @@ import org.mockito.Mockito;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientSettings;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@ -56,6 +57,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
private ClientRegistration registration2;
private ClientRegistration pkceClientRegistration;
private ClientRegistration fineRedirectUriTemplateRegistration;
private ClientRegistration publicClientRegistration;
@ -72,6 +75,9 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
public void setUp() {
this.registration1 = TestClientRegistrations.clientRegistration().build();
this.registration2 = TestClientRegistrations.clientRegistration2().build();
this.pkceClientRegistration = pkceClientRegistration().build();
this.fineRedirectUriTemplateRegistration = fineRedirectUriTemplateClientRegistration().build();
// @formatter:off
this.publicClientRegistration = TestClientRegistrations.clientRegistration()
@ -86,8 +92,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
.build();
// @formatter:on
this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1,
this.registration2, this.fineRedirectUriTemplateRegistration, this.publicClientRegistration,
this.oidcRegistration);
this.registration2, this.pkceClientRegistration, this.fineRedirectUriTemplateRegistration,
this.publicClientRegistration, this.oidcRegistration);
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository,
this.authorizationRequestBaseUri);
}
@ -563,6 +569,32 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "appid=client-id");
}
@Test
public void resolveWhenAuthorizationRequestProvideCodeChallengeMethod() {
ClientRegistration clientRegistration = this.pkceClientRegistration;
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
assertThat(authorizationRequest.getAdditionalParameters().containsKey(PkceParameterNames.CODE_CHALLENGE_METHOD))
.isTrue();
}
private static ClientRegistration.Builder pkceClientRegistration() {
return ClientRegistration.withRegistrationId("pkce")
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
.clientSettings(ClientSettings.builder().requireProofKey(true).build())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("read:user")
.authorizationUri("https://example.com/login/oauth/authorize")
.tokenUri("https://example.com/login/oauth/access_token")
.userInfoUri("https://api.example.com/user")
.userNameAttributeName("id")
.clientName("Client Name")
.clientId("client-id-3")
.clientSecret("client-secret");
}
private static ClientRegistration.Builder fineRedirectUriTemplateClientRegistration() {
// @formatter:off
return ClientRegistration.withRegistrationId("fine-redirect-uri-template-client-registration")