Skip to content

Commit 8a04cda

Browse files
committed
Closes gh-11440
1 parent 499c920 commit 8a04cda

File tree

6 files changed

+155
-122
lines changed

6 files changed

+155
-122
lines changed

Diff for: 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

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

+3-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,8 @@
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 = DefaultOAuth2TokenRequestHeadersConverter
46+
.historicalConverter();
5047

5148
private Converter<T, MultiValueMap<String, String>> parametersConverter = this::createParameters;
5249

Diff for: oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java

+3-34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 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,17 +16,13 @@
1616

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

19-
import java.io.UnsupportedEncodingException;
20-
import java.net.URLEncoder;
21-
import java.nio.charset.StandardCharsets;
2219
import java.util.Collections;
2320
import java.util.Set;
2421

2522
import reactor.core.publisher.Mono;
2623

2724
import org.springframework.core.convert.converter.Converter;
2825
import org.springframework.http.HttpHeaders;
29-
import org.springframework.http.MediaType;
3026
import org.springframework.http.ReactiveHttpInputMessage;
3127
import org.springframework.security.oauth2.client.registration.ClientRegistration;
3228
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -65,6 +61,7 @@
6561
* @see WebClientReactiveClientCredentialsTokenResponseClient
6662
* @see WebClientReactivePasswordTokenResponseClient
6763
* @see WebClientReactiveRefreshTokenTokenResponseClient
64+
* @see DefaultOAuth2TokenRequestHeadersConverter
6865
*/
6966
public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
7067
implements ReactiveOAuth2AccessTokenResponseClient<T> {
@@ -73,7 +70,7 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T
7370

7471
private Converter<T, RequestHeadersSpec<?>> requestEntityConverter = this::validatingPopulateRequest;
7572

76-
private Converter<T, HttpHeaders> headersConverter = this::populateTokenRequestHeaders;
73+
private Converter<T, HttpHeaders> headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>();
7774

7875
private Converter<T, MultiValueMap<String, String>> parametersConverter = this::populateTokenRequestParameters;
7976

@@ -131,34 +128,6 @@ private RequestHeadersSpec<?> populateRequest(T grantRequest) {
131128
.body(createTokenRequestBody(grantRequest));
132129
}
133130

134-
/**
135-
* Populates the headers for the token request.
136-
* @param grantRequest the grant request
137-
* @return the headers populated for the token request
138-
*/
139-
private HttpHeaders populateTokenRequestHeaders(T grantRequest) {
140-
HttpHeaders headers = new HttpHeaders();
141-
ClientRegistration clientRegistration = clientRegistration(grantRequest);
142-
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
143-
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
144-
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
145-
String clientId = encodeClientCredential(clientRegistration.getClientId());
146-
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
147-
headers.setBasicAuth(clientId, clientSecret);
148-
}
149-
return headers;
150-
}
151-
152-
private static String encodeClientCredential(String clientCredential) {
153-
try {
154-
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
155-
}
156-
catch (UnsupportedEncodingException ex) {
157-
// Will not happen since UTF-8 is a standard charset
158-
throw new IllegalArgumentException(ex);
159-
}
160-
}
161-
162131
/**
163132
* Populates default parameters for the token request.
164133
* @param grantRequest the grant request
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.client.endpoint;
18+
19+
import org.springframework.core.convert.converter.Converter;
20+
import org.springframework.http.HttpHeaders;
21+
import org.springframework.http.MediaType;
22+
import org.springframework.http.RequestEntity;
23+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
24+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
25+
26+
import java.net.URLEncoder;
27+
import java.nio.charset.StandardCharsets;
28+
import java.util.Collections;
29+
30+
/**
31+
* Default {@link Converter} used to convert an
32+
* {@link AbstractOAuth2AuthorizationGrantRequest} to the {@link HttpHeaders} of aKk
33+
* {@link RequestEntity} representation of an OAuth 2.0 Access Token Request for the
34+
* specific Authorization Grant.
35+
*
36+
* @author Peter Eastham
37+
* @author Joe Grandja
38+
* @see AbstractOAuth2AuthorizationGrantRequestEntityConverter
39+
* @since 6.3
40+
*/
41+
public final class DefaultOAuth2TokenRequestHeadersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
42+
implements Converter<T, HttpHeaders> {
43+
44+
private MediaType accept = MediaType.APPLICATION_JSON;
45+
46+
private MediaType contentType = MediaType.APPLICATION_FORM_URLENCODED;
47+
48+
private boolean encodeClientCredentialsIfRequired = true;
49+
50+
/**
51+
* Populates the headers for the token request.
52+
* @param grantRequest the grant request
53+
* @return the headers populated for the token request
54+
*/
55+
@Override
56+
public HttpHeaders convert(T grantRequest) {
57+
HttpHeaders headers = new HttpHeaders();
58+
headers.setAccept(Collections.singletonList(accept));
59+
headers.setContentType(contentType);
60+
ClientRegistration clientRegistration = grantRequest.getClientRegistration();
61+
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
62+
String clientId = encodeClientCredential(clientRegistration.getClientId());
63+
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
64+
headers.setBasicAuth(clientId, clientSecret);
65+
}
66+
return headers;
67+
}
68+
69+
private String encodeClientCredential(String clientCredential) {
70+
String encodedCredential = clientCredential;
71+
if (this.encodeClientCredentialsIfRequired) {
72+
encodedCredential = URLEncoder.encode(clientCredential, StandardCharsets.UTF_8);
73+
}
74+
return encodedCredential;
75+
}
76+
77+
/**
78+
* Sets the behavior for if this URL Encoding the Client Credentials during the
79+
* conversion.
80+
* @param encodeClientCredentialsIfRequired if false, no URL encoding will happen
81+
*/
82+
public void setEncodeClientCredentials(boolean encodeClientCredentialsIfRequired) {
83+
this.encodeClientCredentialsIfRequired = encodeClientCredentialsIfRequired;
84+
}
85+
86+
/**
87+
* MediaType to set for the Accept header. Default is application/json
88+
* @param accept MediaType to use for the Accept header
89+
*/
90+
private void setAccept(MediaType accept) {
91+
this.accept = accept;
92+
}
93+
94+
/**
95+
* MediaType to set for the Content Type header. Default is
96+
* application/x-www-form-urlencoded
97+
* @param contentType MediaType to use for the Content Type header
98+
*/
99+
private void setContentType(MediaType contentType) {
100+
this.contentType = contentType;
101+
}
102+
103+
static <T extends AbstractOAuth2AuthorizationGrantRequest> DefaultOAuth2TokenRequestHeadersConverter<T> historicalConverter() {
104+
DefaultOAuth2TokenRequestHeadersConverter<T> converter = new DefaultOAuth2TokenRequestHeadersConverter<>();
105+
converter.setAccept(MediaType.APPLICATION_JSON_UTF8);
106+
converter.setContentType(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
107+
return converter;
108+
}
109+
110+
}

Diff for: oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationGrantRequestEntityUtils.java

-78
This file was deleted.

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

+36-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,7 +110,10 @@ 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");
116119
RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
@@ -121,7 +124,7 @@ public void convertWhenGrantRequestValidThenConverts() {
121124
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
122125
assertThat(headers.getContentType())
123126
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
124-
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
127+
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0JTNE");
125128
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
126129
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
127130
.isEqualTo(AuthorizationGrantType.PASSWORD.getValue());
@@ -130,4 +133,34 @@ public void convertWhenGrantRequestValidThenConverts() {
130133
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
131134
}
132135

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

0 commit comments

Comments
 (0)