Jelajahi Sumber

Add ClientRegistration.clientSettings.requireProofKey to Enable PKCE

Closes gh-16382

Signed-off-by: DingHao <dh.hiekn@gmail.com>
DingHao 7 bulan lalu
induk
melakukan
8d3e0844c5

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

@@ -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.

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

@@ -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;

+ 68 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientSettings.java

@@ -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;
+		}
+
+	}
+
+}

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

@@ -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;

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

@@ -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
 	}

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

@@ -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")