浏览代码

Polish gh-14859

Steve Riesenberg 1 年之前
父节点
当前提交
2598bf8c37

+ 0 - 3
docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc

@@ -92,9 +92,6 @@ val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
 tokenResponseClient.setRequestEntityConverter(requestEntityConverter)
 ----
 ======
-[NOTE]
-If you're using the `client-authentication-method: client_secret_basic` and you need to skip URL encoding,
-create a new `DefaultOAuth2TokenRequestHeadersConverter` and set it in the Request Entity Converter above.
 
 === Authenticate using `client_secret_jwt`
 

+ 1 - 2
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractOAuth2AuthorizationGrantRequestEntityConverter.java

@@ -42,8 +42,7 @@ import org.springframework.web.util.UriComponentsBuilder;
 abstract class AbstractOAuth2AuthorizationGrantRequestEntityConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
 		implements Converter<T, RequestEntity<?>> {
 
-	private Converter<T, HttpHeaders> headersConverter = DefaultOAuth2TokenRequestHeadersConverter
-			.historicalConverter();
+	private Converter<T, HttpHeaders> headersConverter = DefaultOAuth2TokenRequestHeadersConverter.withCharsetUtf8();
 
 	private Converter<T, MultiValueMap<String, String>> parametersConverter = this::createParameters;
 

+ 42 - 43
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultOAuth2TokenRequestHeadersConverter.java

@@ -16,6 +16,10 @@
 
 package org.springframework.security.oauth2.client.endpoint;
 
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
@@ -23,87 +27,82 @@ import org.springframework.http.RequestEntity;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-
 /**
  * Default {@link Converter} used to convert an
- * {@link AbstractOAuth2AuthorizationGrantRequest} to the {@link HttpHeaders} of aKk
+ * {@link AbstractOAuth2AuthorizationGrantRequest} to the {@link HttpHeaders} of a
  * {@link RequestEntity} representation of an OAuth 2.0 Access Token Request for the
  * specific Authorization Grant.
  *
  * @author Peter Eastham
- * @author Joe Grandja
- * @see AbstractOAuth2AuthorizationGrantRequestEntityConverter
+ * @author Steve Riesenberg
  * @since 6.3
+ * @see AbstractOAuth2AuthorizationGrantRequestEntityConverter
  */
 public final class DefaultOAuth2TokenRequestHeadersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
 		implements Converter<T, HttpHeaders> {
 
-	private MediaType accept = MediaType.APPLICATION_JSON;
+	private static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON,
+			StandardCharsets.UTF_8);
+
+	private static final MediaType APPLICATION_FORM_URLENCODED_UTF8 = new MediaType(
+			MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8);
+
+	private List<MediaType> accept = List.of(MediaType.APPLICATION_JSON);
 
 	private MediaType contentType = MediaType.APPLICATION_FORM_URLENCODED;
 
-	private boolean encodeClientCredentialsIfRequired = true;
+	private boolean encodeClientCredentials = true;
 
 	/**
-	 * Populates the headers for the token request.
-	 * @param grantRequest the grant request
+	 * Populates the default headers for the token request.
+	 * @param grantRequest the authorization grant request
 	 * @return the headers populated for the token request
 	 */
 	@Override
 	public HttpHeaders convert(T grantRequest) {
 		HttpHeaders headers = new HttpHeaders();
-		headers.setAccept(Collections.singletonList(accept));
-		headers.setContentType(contentType);
+		headers.setAccept(this.accept);
+		headers.setContentType(this.contentType);
 		ClientRegistration clientRegistration = grantRequest.getClientRegistration();
 		if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
-			String clientId = encodeClientCredential(clientRegistration.getClientId());
-			String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
+			String clientId = encodeClientCredentialIfRequired(clientRegistration.getClientId());
+			String clientSecret = encodeClientCredentialIfRequired(clientRegistration.getClientSecret());
 			headers.setBasicAuth(clientId, clientSecret);
 		}
 		return headers;
 	}
 
-	private String encodeClientCredential(String clientCredential) {
-		String encodedCredential = clientCredential;
-		if (this.encodeClientCredentialsIfRequired) {
-			encodedCredential = URLEncoder.encode(clientCredential, StandardCharsets.UTF_8);
+	private String encodeClientCredentialIfRequired(String clientCredential) {
+		if (!this.encodeClientCredentials) {
+			return clientCredential;
 		}
-		return encodedCredential;
-	}
-
-	/**
-	 * Sets the behavior for if this URL Encoding the Client Credentials during the
-	 * conversion.
-	 * @param encodeClientCredentialsIfRequired if false, no URL encoding will happen
-	 */
-	public void setEncodeClientCredentials(boolean encodeClientCredentialsIfRequired) {
-		this.encodeClientCredentialsIfRequired = encodeClientCredentialsIfRequired;
+		return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8);
 	}
 
 	/**
-	 * MediaType to set for the Accept header. Default is application/json
-	 * @param accept MediaType to use for the Accept header
+	 * Sets whether the client credentials of the {@code Authorization} header will be
+	 * encoded using the {@code application/x-www-form-urlencoded} encoding algorithm
+	 * according to RFC 6749. Default is {@code true}.
+	 * @param encodeClientCredentials whether the client credentials will be encoded
+	 * @see <a target="_blank" href=
+	 * "https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1">2.3.1 Client
+	 * Password</a>
 	 */
-	private void setAccept(MediaType accept) {
-		this.accept = accept;
+	public void setEncodeClientCredentials(boolean encodeClientCredentials) {
+		this.encodeClientCredentials = encodeClientCredentials;
 	}
 
 	/**
-	 * MediaType to set for the Content Type header. Default is
-	 * application/x-www-form-urlencoded
-	 * @param contentType MediaType to use for the Content Type header
+	 * Creates a {@link DefaultOAuth2TokenRequestHeadersConverter} that populates default
+	 * {@link HttpHeaders} that includes {@code charset=UTF-8} on both the {@code Accept}
+	 * and {@code Content-Type} headers to provide backwards compatibility for
+	 * {@link AbstractOAuth2AuthorizationGrantRequestEntityConverter}.
+	 * @return the default headers converter
 	 */
-	private void setContentType(MediaType contentType) {
-		this.contentType = contentType;
-	}
-
-	static <T extends AbstractOAuth2AuthorizationGrantRequest> DefaultOAuth2TokenRequestHeadersConverter<T> historicalConverter() {
+	static <T extends AbstractOAuth2AuthorizationGrantRequest> DefaultOAuth2TokenRequestHeadersConverter<T> withCharsetUtf8() {
 		DefaultOAuth2TokenRequestHeadersConverter<T> converter = new DefaultOAuth2TokenRequestHeadersConverter<>();
-		converter.setAccept(MediaType.APPLICATION_JSON_UTF8);
-		converter.setContentType(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
+		converter.accept = List.of(APPLICATION_JSON_UTF8);
+		converter.contentType = APPLICATION_FORM_URLENCODED_UTF8;
 		return converter;
 	}
 

+ 119 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultOAuth2TokenRequestHeadersConverterTests.java

@@ -0,0 +1,119 @@
+/*
+ * Copyright 2002-2024 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.endpoint;
+
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link DefaultOAuth2TokenRequestHeadersConverter}.
+ *
+ * @author Steve Riesenberg
+ */
+public class DefaultOAuth2TokenRequestHeadersConverterTests {
+
+	private static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON,
+			StandardCharsets.UTF_8);
+
+	private static final MediaType APPLICATION_FORM_URLENCODED_UTF8 = new MediaType(
+			MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8);
+
+	private DefaultOAuth2TokenRequestHeadersConverter<OAuth2ClientCredentialsGrantRequest> converter;
+
+	@BeforeEach
+	public void setUp() {
+		this.converter = new DefaultOAuth2TokenRequestHeadersConverter<>();
+	}
+
+	@Test
+	public void convertWhenEncodeClientCredentialsTrueThenConvertsWithUrlEncoding() {
+		// @formatter:off
+		ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
+				.clientId("clientId")
+				.clientSecret("clientSecret=")
+				.build();
+		// @formatter:on
+		OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration);
+		HttpHeaders defaultHeaders = this.converter.convert(grantRequest);
+		assertThat(defaultHeaders.getAccept()).containsExactly(MediaType.APPLICATION_JSON);
+		assertThat(defaultHeaders.getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
+		assertThat(defaultHeaders.getFirst(HttpHeaders.AUTHORIZATION))
+			.isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0JTNE");
+	}
+
+	@Test
+	public void convertWhenEncodeClientCredentialsFalseThenConvertsWithoutUrlEncoding() {
+		this.converter.setEncodeClientCredentials(false);
+		// @formatter:off
+		ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
+				.clientId("clientId")
+				.clientSecret("clientSecret=")
+				.build();
+		// @formatter:on
+		OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration);
+		HttpHeaders defaultHeaders = this.converter.convert(grantRequest);
+		assertThat(defaultHeaders.getAccept()).containsExactly(MediaType.APPLICATION_JSON);
+		assertThat(defaultHeaders.getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
+		assertThat(defaultHeaders.getFirst(HttpHeaders.AUTHORIZATION))
+			.isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0PQ==");
+	}
+
+	@Test
+	public void convertWhenWithCharsetUtf8AndEncodeClientCredentialsTrueThenConvertsWithUrlEncoding() {
+		this.converter = DefaultOAuth2TokenRequestHeadersConverter.withCharsetUtf8();
+		// @formatter:off
+		ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
+				.clientId("clientId")
+				.clientSecret("clientSecret=")
+				.build();
+		// @formatter:on
+		OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration);
+		HttpHeaders defaultHeaders = this.converter.convert(grantRequest);
+		assertThat(defaultHeaders.getAccept()).containsExactly(APPLICATION_JSON_UTF8);
+		assertThat(defaultHeaders.getContentType()).isEqualTo(APPLICATION_FORM_URLENCODED_UTF8);
+		assertThat(defaultHeaders.getFirst(HttpHeaders.AUTHORIZATION))
+			.isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0JTNE");
+	}
+
+	@Test
+	public void convertWhenWithCharsetUtf8EncodeClientCredentialsFalseThenConvertsWithoutUrlEncoding() {
+		this.converter = DefaultOAuth2TokenRequestHeadersConverter.withCharsetUtf8();
+		this.converter.setEncodeClientCredentials(false);
+		// @formatter:off
+		ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
+				.clientId("clientId")
+				.clientSecret("clientSecret=")
+				.build();
+		// @formatter:on
+		OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration);
+		HttpHeaders defaultHeaders = this.converter.convert(grantRequest);
+		assertThat(defaultHeaders.getAccept()).containsExactly(APPLICATION_JSON_UTF8);
+		assertThat(defaultHeaders.getContentType()).isEqualTo(APPLICATION_FORM_URLENCODED_UTF8);
+		assertThat(defaultHeaders.getFirst(HttpHeaders.AUTHORIZATION))
+			.isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0PQ==");
+	}
+
+}

+ 8 - 3
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -122,7 +122,12 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
 	@SuppressWarnings("unchecked")
 	@Test
 	public void convertWhenGrantRequestValidThenConverts() {
-		ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
+		// @formatter:off
+		ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
+				.clientId("clientId")
+				.clientSecret("clientSecret=")
+				.build();
+		// @formatter:on
 		OAuth2AuthorizationExchange authorizationExchange = TestOAuth2AuthorizationExchanges.success();
 		OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest();
 		OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse();
@@ -136,7 +141,7 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests {
 		assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
 		assertThat(headers.getContentType())
 			.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
-		assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
+		assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0JTNE");
 		MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
 		assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());

+ 3 - 36
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -110,10 +110,7 @@ public class OAuth2PasswordGrantRequestEntityConverterTests {
 	@SuppressWarnings("unchecked")
 	@Test
 	public void convertWhenGrantRequestValidThenConverts() {
-		ClientRegistration clientRegistration = TestClientRegistrations.password()
-			.clientId("clientId")
-			.clientSecret("clientSecret=")
-			.build();
+		ClientRegistration clientRegistration = TestClientRegistrations.password().build();
 		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1",
 				"password");
 		RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
@@ -124,7 +121,7 @@ public class OAuth2PasswordGrantRequestEntityConverterTests {
 		assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
 		assertThat(headers.getContentType())
 			.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
-		assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0JTNE");
+		assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
 		MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
 		assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
 			.isEqualTo(AuthorizationGrantType.PASSWORD.getValue());
@@ -133,34 +130,4 @@ public class OAuth2PasswordGrantRequestEntityConverterTests {
 		assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
 	}
 
-	@SuppressWarnings("unchecked")
-	@Test
-	public void convertWhenGrantRequestValidThenConvertsWithoutUrlEncoding() {
-		ClientRegistration clientRegistration = TestClientRegistrations.password()
-			.clientId("clientId")
-			.clientSecret("clientSecret=")
-			.build();
-		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1",
-				"password=");
-		DefaultOAuth2TokenRequestHeadersConverter<OAuth2PasswordGrantRequest> headersConverter = DefaultOAuth2TokenRequestHeadersConverter
-				.historicalConverter();
-		headersConverter.setEncodeClientCredentials(false);
-		this.converter.setHeadersConverter(headersConverter);
-		RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
-		assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
-		assertThat(requestEntity.getUrl().toASCIIString())
-			.isEqualTo(clientRegistration.getProviderDetails().getTokenUri());
-		HttpHeaders headers = requestEntity.getHeaders();
-		assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
-		assertThat(headers.getContentType())
-			.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
-		assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0PQ==");
-		MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
-		assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
-			.isEqualTo(AuthorizationGrantType.PASSWORD.getValue());
-		assertThat(formParameters.getFirst(OAuth2ParameterNames.USERNAME)).isEqualTo("user1");
-		assertThat(formParameters.getFirst(OAuth2ParameterNames.PASSWORD)).isEqualTo("password=");
-		assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
-	}
-
 }