Skip to content

Commit 0a61b6b

Browse files
committed
Fix OAuth2 client-secret encoding with Base64 padding
When using CLIENT_SECRET_POST authentication method with a client secret that ends with Base64 padding characters ('='), the padding was being URL-encoded to '%3D'. This caused authentication failures with some OAuth2 providers that expect the padding characters to remain unencoded. This commit adds special handling for CLIENT_SECRET_POST authentication to preserve Base64 padding characters in the client secret while still properly encoding other form parameters. Closes gh-17629 Signed-off-by: Hyunjoon Kim <[email protected]> Signed-off-by: academey <[email protected]>
1 parent 1d2d268 commit 0a61b6b

File tree

2 files changed

+95
-0
lines changed

2 files changed

+95
-0
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121
import org.springframework.core.convert.converter.Converter;
2222
import org.springframework.http.HttpHeaders;
23+
import org.springframework.http.MediaType;
2324
import org.springframework.http.converter.FormHttpMessageConverter;
25+
import org.springframework.http.converter.StringHttpMessageConverter;
2426
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
2527
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2628
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -66,6 +68,7 @@ public abstract class AbstractRestClientOAuth2AccessTokenResponseClient<T extend
6668
.messageConverters((messageConverters) -> {
6769
messageConverters.clear();
6870
messageConverters.add(new FormHttpMessageConverter());
71+
messageConverters.add(new StringHttpMessageConverter());
6972
messageConverters.add(new OAuth2AccessTokenResponseHttpMessageConverter());
7073
})
7174
.defaultStatusHandler(new OAuth2ErrorResponseErrorHandler())
@@ -134,6 +137,22 @@ private RequestHeadersSpec<?> populateRequest(T grantRequest) {
134137
}
135138
this.parametersCustomizer.accept(parameters);
136139

140+
// Special handling for CLIENT_SECRET_POST to preserve Base64 padding
141+
ClientRegistration clientRegistration = grantRequest.getClientRegistration();
142+
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())) {
143+
String formData = OAuth2ClientCredentialsGrantRequestEntityUtils.encodeFormData(parameters);
144+
return this.restClient.post()
145+
.uri(clientRegistration.getProviderDetails().getTokenUri())
146+
.headers((headers) -> {
147+
HttpHeaders headersToAdd = this.headersConverter.convert(grantRequest);
148+
if (headersToAdd != null) {
149+
headers.addAll(headersToAdd);
150+
}
151+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
152+
})
153+
.body(formData);
154+
}
155+
137156
return this.restClient.post()
138157
.uri(grantRequest.getClientRegistration().getProviderDetails().getTokenUri())
139158
.headers((headers) -> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2025 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 java.io.UnsupportedEncodingException;
20+
import java.net.URLEncoder;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.StringJoiner;
23+
24+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
25+
import org.springframework.util.MultiValueMap;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* Utility methods for the OAuth 2.0 Client Credentials Grant.
30+
*
31+
* @author Hyunjoon Kim
32+
* @since 6.5
33+
*/
34+
final class OAuth2ClientCredentialsGrantRequestEntityUtils {
35+
36+
private OAuth2ClientCredentialsGrantRequestEntityUtils() {
37+
}
38+
39+
static String encodeFormData(MultiValueMap<String, String> parameters) {
40+
StringJoiner result = new StringJoiner("&");
41+
parameters.forEach((key, values) -> {
42+
for (String value : values) {
43+
result.add(encodeFormParameter(key, value));
44+
}
45+
});
46+
return result.toString();
47+
}
48+
49+
private static String encodeFormParameter(String name, String value) {
50+
if (!StringUtils.hasText(value)) {
51+
return urlEncode(name);
52+
}
53+
54+
// Special handling for client_secret to preserve Base64 padding
55+
if (OAuth2ParameterNames.CLIENT_SECRET.equals(name) && value.endsWith("=")) {
56+
// For client secrets ending with '=', don't encode the padding character
57+
int lastEqualIndex = value.lastIndexOf('=');
58+
String beforePadding = value.substring(0, lastEqualIndex);
59+
String padding = value.substring(lastEqualIndex);
60+
return urlEncode(name) + "=" + urlEncode(beforePadding) + padding;
61+
}
62+
63+
return urlEncode(name) + "=" + urlEncode(value);
64+
}
65+
66+
private static String urlEncode(String value) {
67+
try {
68+
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
69+
}
70+
catch (UnsupportedEncodingException ex) {
71+
// Should never happen with UTF-8
72+
throw new IllegalArgumentException(ex);
73+
}
74+
}
75+
76+
}

0 commit comments

Comments
 (0)