2
0
Эх сурвалжийг харах

Simplify Disabling Encoding Client ID and Secret

Closes gh-11440
Crain-32 1 жил өмнө
parent
commit
d0adb2aa70

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

@@ -92,7 +92,9 @@ 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`
 

+ 3 - 6
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractOAuth2AuthorizationGrantRequestEntityConverter.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.
@@ -42,11 +42,8 @@ import org.springframework.web.util.UriComponentsBuilder;
 abstract class AbstractOAuth2AuthorizationGrantRequestEntityConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
 		implements Converter<T, RequestEntity<?>> {
 
-	// @formatter:off
-	private Converter<T, HttpHeaders> headersConverter =
-			(authorizationGrantRequest) -> OAuth2AuthorizationGrantRequestEntityUtils
-					.getTokenRequestHeaders(authorizationGrantRequest.getClientRegistration());
-	// @formatter:on
+	private Converter<T, HttpHeaders> headersConverter = DefaultOAuth2TokenRequestHeadersConverter
+			.historicalConverter();
 
 	private Converter<T, MultiValueMap<String, String>> parametersConverter = this::createParameters;
 

+ 3 - 34
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2023 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.
@@ -16,9 +16,6 @@
 
 package org.springframework.security.oauth2.client.endpoint;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.Set;
 
@@ -26,7 +23,6 @@ import reactor.core.publisher.Mono;
 
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
 import org.springframework.http.ReactiveHttpInputMessage;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -65,6 +61,7 @@ import org.springframework.web.reactive.function.client.WebClient.RequestHeaders
  * @see WebClientReactiveClientCredentialsTokenResponseClient
  * @see WebClientReactivePasswordTokenResponseClient
  * @see WebClientReactiveRefreshTokenTokenResponseClient
+ * @see DefaultOAuth2TokenRequestHeadersConverter
  */
 public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
 		implements ReactiveOAuth2AccessTokenResponseClient<T> {
@@ -73,7 +70,7 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T
 
 	private Converter<T, RequestHeadersSpec<?>> requestEntityConverter = this::validatingPopulateRequest;
 
-	private Converter<T, HttpHeaders> headersConverter = this::populateTokenRequestHeaders;
+	private Converter<T, HttpHeaders> headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>();
 
 	private Converter<T, MultiValueMap<String, String>> parametersConverter = this::populateTokenRequestParameters;
 
@@ -131,34 +128,6 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T
 			.body(createTokenRequestBody(grantRequest));
 	}
 
-	/**
-	 * Populates the headers for the token request.
-	 * @param grantRequest the grant request
-	 * @return the headers populated for the token request
-	 */
-	private HttpHeaders populateTokenRequestHeaders(T grantRequest) {
-		HttpHeaders headers = new HttpHeaders();
-		ClientRegistration clientRegistration = clientRegistration(grantRequest);
-		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
-		headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
-		if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
-			String clientId = encodeClientCredential(clientRegistration.getClientId());
-			String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
-			headers.setBasicAuth(clientId, clientSecret);
-		}
-		return headers;
-	}
-
-	private static String encodeClientCredential(String clientCredential) {
-		try {
-			return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
-		}
-		catch (UnsupportedEncodingException ex) {
-			// Will not happen since UTF-8 is a standard charset
-			throw new IllegalArgumentException(ex);
-		}
-	}
-
 	/**
 	 * Populates default parameters for the token request.
 	 * @param grantRequest the grant request

+ 110 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultOAuth2TokenRequestHeadersConverter.java

@@ -0,0 +1,110 @@
+/*
+ * 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 org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+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 RequestEntity} representation of an OAuth 2.0 Access Token Request for the
+ * specific Authorization Grant.
+ *
+ * @author Peter Eastham
+ * @author Joe Grandja
+ * @see AbstractOAuth2AuthorizationGrantRequestEntityConverter
+ * @since 6.3
+ */
+public final class DefaultOAuth2TokenRequestHeadersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
+		implements Converter<T, HttpHeaders> {
+
+	private MediaType accept = MediaType.APPLICATION_JSON;
+
+	private MediaType contentType = MediaType.APPLICATION_FORM_URLENCODED;
+
+	private boolean encodeClientCredentialsIfRequired = true;
+
+	/**
+	 * Populates the headers for the token request.
+	 * @param grantRequest the 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);
+		ClientRegistration clientRegistration = grantRequest.getClientRegistration();
+		if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
+			String clientId = encodeClientCredential(clientRegistration.getClientId());
+			String clientSecret = encodeClientCredential(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);
+		}
+		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;
+	}
+
+	/**
+	 * MediaType to set for the Accept header. Default is application/json
+	 * @param accept MediaType to use for the Accept header
+	 */
+	private void setAccept(MediaType accept) {
+		this.accept = accept;
+	}
+
+	/**
+	 * 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
+	 */
+	private void setContentType(MediaType contentType) {
+		this.contentType = contentType;
+	}
+
+	static <T extends AbstractOAuth2AuthorizationGrantRequest> DefaultOAuth2TokenRequestHeadersConverter<T> historicalConverter() {
+		DefaultOAuth2TokenRequestHeadersConverter<T> converter = new DefaultOAuth2TokenRequestHeadersConverter<>();
+		converter.setAccept(MediaType.APPLICATION_JSON_UTF8);
+		converter.setContentType(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
+		return converter;
+	}
+
+}

+ 0 - 78
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationGrantRequestEntityUtils.java

@@ -1,78 +0,0 @@
-/*
- * Copyright 2002-2022 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.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-
-import org.springframework.core.convert.converter.Converter;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.http.RequestEntity;
-import org.springframework.security.oauth2.client.registration.ClientRegistration;
-import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
-
-/**
- * Utility methods used by the {@link Converter}'s that convert from an implementation of
- * an {@link AbstractOAuth2AuthorizationGrantRequest} to a {@link RequestEntity}
- * representation of an OAuth 2.0 Access Token Request for the specific Authorization
- * Grant.
- *
- * @author Joe Grandja
- * @since 5.1
- * @see OAuth2AuthorizationCodeGrantRequestEntityConverter
- * @see OAuth2ClientCredentialsGrantRequestEntityConverter
- */
-final class OAuth2AuthorizationGrantRequestEntityUtils {
-
-	private static HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders();
-
-	private OAuth2AuthorizationGrantRequestEntityUtils() {
-	}
-
-	static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {
-		HttpHeaders headers = new HttpHeaders();
-		headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
-		if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
-			String clientId = encodeClientCredential(clientRegistration.getClientId());
-			String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
-			headers.setBasicAuth(clientId, clientSecret);
-		}
-		return headers;
-	}
-
-	private static String encodeClientCredential(String clientCredential) {
-		try {
-			return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
-		}
-		catch (UnsupportedEncodingException ex) {
-			// Will not happen since UTF-8 is a standard charset
-			throw new IllegalArgumentException(ex);
-		}
-	}
-
-	private static HttpHeaders getDefaultTokenRequestHeaders() {
-		HttpHeaders headers = new HttpHeaders();
-		headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
-		final MediaType contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
-		headers.setContentType(contentType);
-		return headers;
-	}
-
-}

+ 36 - 3
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2PasswordGrantRequestEntityConverterTests.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.
@@ -110,7 +110,10 @@ public class OAuth2PasswordGrantRequestEntityConverterTests {
 	@SuppressWarnings("unchecked")
 	@Test
 	public void convertWhenGrantRequestValidThenConverts() {
-		ClientRegistration clientRegistration = TestClientRegistrations.password().build();
+		ClientRegistration clientRegistration = TestClientRegistrations.password()
+			.clientId("clientId")
+			.clientSecret("clientSecret=")
+			.build();
 		OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1",
 				"password");
 		RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
@@ -121,7 +124,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)).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.PASSWORD.getValue());
@@ -130,4 +133,34 @@ 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());
+	}
+
 }