Skip to content

Commit cff1efd

Browse files
committed
Closes gh-11440
1 parent 8f87732 commit cff1efd

File tree

4 files changed

+73
-38
lines changed

4 files changed

+73
-38
lines changed

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
9292
tokenResponseClient.setRequestEntityConverter(requestEntityConverter)
9393
----
9494
======
95-
95+
[NOTE]
96+
If you're using the `client-authentication-method: client_secret_basic` and you need to skip URL encoding,
97+
create a new `DefaultOAuth2TokenRequestHeadersConverter` and set it in the Request Entity Converter above.
9698

9799
=== Authenticate using `client_secret_jwt`
98100

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

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -42,11 +42,7 @@
4242
abstract class AbstractOAuth2AuthorizationGrantRequestEntityConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
4343
implements Converter<T, RequestEntity<?>> {
4444

45-
// @formatter:off
46-
private Converter<T, HttpHeaders> headersConverter =
47-
(authorizationGrantRequest) -> OAuth2AuthorizationGrantRequestEntityUtils
48-
.getTokenRequestHeaders(authorizationGrantRequest.getClientRegistration());
49-
// @formatter:on
45+
private Converter<T, HttpHeaders> headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>();
5046

5147
private Converter<T, MultiValueMap<String, String>> parametersConverter = this::createParameters;
5248

Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.security.oauth2.client.endpoint;
1818

19-
import java.io.UnsupportedEncodingException;
2019
import java.net.URLEncoder;
2120
import java.nio.charset.StandardCharsets;
2221
import java.util.Collections;
@@ -29,50 +28,54 @@
2928
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
3029

3130
/**
32-
* Utility methods used by the {@link Converter}'s that convert from an implementation of
33-
* an {@link AbstractOAuth2AuthorizationGrantRequest} to a {@link RequestEntity}
34-
* representation of an OAuth 2.0 Access Token Request for the specific Authorization
35-
* Grant.
31+
* Default Converter used by the
32+
* {@link OAuth2AuthorizationCodeGrantRequestEntityConverter} that convert from an
33+
* implementation of an {@link AbstractOAuth2AuthorizationGrantRequest} to a
34+
* {@link RequestEntity} representation of an OAuth 2.0 Access Token Request for the
35+
* specific Authorization Grant.
3636
*
37+
* @author Peter Eastham
3738
* @author Joe Grandja
38-
* @since 5.1
39-
* @see OAuth2AuthorizationCodeGrantRequestEntityConverter
39+
* @since 6.3
4040
* @see OAuth2ClientCredentialsGrantRequestEntityConverter
4141
*/
42-
final class OAuth2AuthorizationGrantRequestEntityUtils {
42+
public class DefaultOAuth2TokenRequestHeadersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
43+
implements Converter<T, HttpHeaders> {
4344

44-
private static HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders();
45+
private static final HttpHeaders DEFAULT_TOKEN_HEADERS = getDefaultTokenRequestHeaders();
4546

46-
private OAuth2AuthorizationGrantRequestEntityUtils() {
47+
private boolean encodeClientCredentials = true;
48+
49+
private static HttpHeaders getDefaultTokenRequestHeaders() {
50+
HttpHeaders headers = new HttpHeaders();
51+
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
52+
final MediaType contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
53+
headers.setContentType(contentType);
54+
return headers;
4755
}
4856

49-
static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {
57+
@Override
58+
public HttpHeaders convert(T source) {
5059
HttpHeaders headers = new HttpHeaders();
51-
headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
60+
headers.addAll(DEFAULT_TOKEN_HEADERS);
61+
ClientRegistration clientRegistration = source.getClientRegistration();
5262
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
53-
String clientId = encodeClientCredential(clientRegistration.getClientId());
54-
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
63+
String clientId = this.encodeClientCredentials ? encodeClientCredential(clientRegistration.getClientId())
64+
: clientRegistration.getClientId();
65+
String clientSecret = this.encodeClientCredentials
66+
? encodeClientCredential(clientRegistration.getClientSecret())
67+
: clientRegistration.getClientSecret();
5568
headers.setBasicAuth(clientId, clientSecret);
5669
}
5770
return headers;
5871
}
5972

6073
private static String encodeClientCredential(String clientCredential) {
61-
try {
62-
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
63-
}
64-
catch (UnsupportedEncodingException ex) {
65-
// Will not happen since UTF-8 is a standard charset
66-
throw new IllegalArgumentException(ex);
67-
}
74+
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8);
6875
}
6976

70-
private static HttpHeaders getDefaultTokenRequestHeaders() {
71-
HttpHeaders headers = new HttpHeaders();
72-
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
73-
final MediaType contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
74-
headers.setContentType(contentType);
75-
return headers;
77+
public void setEncodeClientCredentials(boolean encodeClientCredentials) {
78+
this.encodeClientCredentials = encodeClientCredentials;
7679
}
7780

7881
}

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

+37-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -110,9 +110,14 @@ public void convertWhenParametersConverterSetThenCalled() {
110110
@SuppressWarnings("unchecked")
111111
@Test
112112
public void convertWhenGrantRequestValidThenConverts() {
113-
ClientRegistration clientRegistration = TestClientRegistrations.password().build();
113+
ClientRegistration clientRegistration = TestClientRegistrations.password()
114+
.clientId("clientId")
115+
.clientSecret("clientSecret=")
116+
.build();
114117
OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1",
115118
"password");
119+
Converter<OAuth2PasswordGrantRequest, HttpHeaders> headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>();
120+
this.converter.setHeadersConverter(headersConverter);
116121
RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
117122
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
118123
assertThat(requestEntity.getUrl().toASCIIString())
@@ -121,7 +126,7 @@ public void convertWhenGrantRequestValidThenConverts() {
121126
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
122127
assertThat(headers.getContentType())
123128
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
124-
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
129+
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0JTNE");
125130
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
126131
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
127132
.isEqualTo(AuthorizationGrantType.PASSWORD.getValue());
@@ -130,4 +135,33 @@ public void convertWhenGrantRequestValidThenConverts() {
130135
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
131136
}
132137

138+
@SuppressWarnings("unchecked")
139+
@Test
140+
public void convertWhenGrantRequestValidThenConvertsWithoutUrlEncoding() {
141+
ClientRegistration clientRegistration = TestClientRegistrations.password()
142+
.clientId("clientId")
143+
.clientSecret("clientSecret=")
144+
.build();
145+
OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1",
146+
"password=");
147+
var headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<OAuth2PasswordGrantRequest>();
148+
headersConverter.setEncodeClientCredentials(false);
149+
this.converter.setHeadersConverter(headersConverter);
150+
RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
151+
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
152+
assertThat(requestEntity.getUrl().toASCIIString())
153+
.isEqualTo(clientRegistration.getProviderDetails().getTokenUri());
154+
HttpHeaders headers = requestEntity.getHeaders();
155+
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
156+
assertThat(headers.getContentType())
157+
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
158+
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0PQ==");
159+
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
160+
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
161+
.isEqualTo(AuthorizationGrantType.PASSWORD.getValue());
162+
assertThat(formParameters.getFirst(OAuth2ParameterNames.USERNAME)).isEqualTo("user1");
163+
assertThat(formParameters.getFirst(OAuth2ParameterNames.PASSWORD)).isEqualTo("password=");
164+
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
165+
}
166+
133167
}

0 commit comments

Comments
 (0)