Skip to content

Commit b5552bc

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

File tree

4 files changed

+69
-44
lines changed

4 files changed

+69
-44
lines changed

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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 2 additions & 6 deletions
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,63 +16,63 @@
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;
22-
import java.util.Collections;
23-
2419
import org.springframework.core.convert.converter.Converter;
2520
import org.springframework.http.HttpHeaders;
2621
import org.springframework.http.MediaType;
2722
import org.springframework.http.RequestEntity;
2823
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2924
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
3025

26+
import java.nio.charset.StandardCharsets;
27+
import java.util.Collections;
28+
import java.net.URLEncoder;
29+
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 {@link OAuth2AuthorizationCodeGrantRequestEntityConverter}
32+
* that convert from an implementation of an {@link AbstractOAuth2AuthorizationGrantRequest}
33+
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request for the
34+
* specific Authorization Grant.
3635
*
36+
* @author Peter Eastham
3737
* @author Joe Grandja
38-
* @since 5.1
39-
* @see OAuth2AuthorizationCodeGrantRequestEntityConverter
38+
* @since 6.3
4039
* @see OAuth2ClientCredentialsGrantRequestEntityConverter
4140
*/
42-
final class OAuth2AuthorizationGrantRequestEntityUtils {
41+
public class DefaultOAuth2TokenRequestHeadersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
42+
implements Converter<T, HttpHeaders> {
4343

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

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

49-
static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {
55+
56+
@Override
57+
public HttpHeaders convert(T source) {
5058
HttpHeaders headers = new HttpHeaders();
51-
headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
59+
headers.addAll(DEFAULT_TOKEN_HEADERS);
60+
ClientRegistration clientRegistration = source.getClientRegistration();
5261
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
53-
String clientId = encodeClientCredential(clientRegistration.getClientId());
54-
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
62+
String clientId = encodeClientCredentials ?
63+
encodeClientCredential(clientRegistration.getClientId()) : clientRegistration.getClientId();
64+
String clientSecret = encodeClientCredentials ?
65+
encodeClientCredential(clientRegistration.getClientSecret()) : clientRegistration.getClientSecret();
5566
headers.setBasicAuth(clientId, clientSecret);
5667
}
5768
return headers;
5869
}
5970

6071
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-
}
68-
}
72+
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8);
73+
}
6974

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;
75+
public void setEncodeClientCredentials(boolean encodeClientCredentials) {
76+
this.encodeClientCredentials = encodeClientCredentials;
7677
}
77-
7878
}

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

Lines changed: 30 additions & 3 deletions
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,11 @@ public void convertWhenParametersConverterSetThenCalled() {
110110
@SuppressWarnings("unchecked")
111111
@Test
112112
public void convertWhenGrantRequestValidThenConverts() {
113-
ClientRegistration clientRegistration = TestClientRegistrations.password().build();
113+
ClientRegistration clientRegistration = TestClientRegistrations.password().clientId("clientId").clientSecret("clientSecret=").build();
114114
OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1",
115115
"password");
116+
Converter<OAuth2PasswordGrantRequest, HttpHeaders> headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>();
117+
this.converter.setHeadersConverter(headersConverter);
116118
RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
117119
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
118120
assertThat(requestEntity.getUrl().toASCIIString())
@@ -121,7 +123,7 @@ public void convertWhenGrantRequestValidThenConverts() {
121123
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
122124
assertThat(headers.getContentType())
123125
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
124-
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
126+
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0JTNE");
125127
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
126128
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
127129
.isEqualTo(AuthorizationGrantType.PASSWORD.getValue());
@@ -130,4 +132,29 @@ public void convertWhenGrantRequestValidThenConverts() {
130132
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
131133
}
132134

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

0 commit comments

Comments
 (0)