Add ClientRegistration.clientSettings.requireProofKey to Enable PKCE
Closes gh-16382 Signed-off-by: DingHao <dh.hiekn@gmail.com>
This commit is contained in:
		
							parent
							
								
									8acd1d3f51
								
							
						
					
					
						commit
						8d3e0844c5
					
				| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue