Skip to content

Commit c020051

Browse files
Steve Riesenbergsjohnr
authored andcommitted
URL encode client credentials
Closes gh-9610
1 parent d5062bb commit c020051

7 files changed

+177
-23
lines changed

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -15,15 +15,18 @@
1515
*/
1616
package org.springframework.security.oauth2.client.endpoint;
1717

18+
import java.io.UnsupportedEncodingException;
19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Collections;
22+
1823
import org.springframework.core.convert.converter.Converter;
1924
import org.springframework.http.HttpHeaders;
2025
import org.springframework.http.MediaType;
2126
import org.springframework.http.RequestEntity;
2227
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2328
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2429

25-
import java.util.Collections;
26-
2730
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
2831

2932
/**
@@ -44,11 +47,23 @@ static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration)
4447
HttpHeaders headers = new HttpHeaders();
4548
headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
4649
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
47-
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
50+
String clientId = encodeClientCredential(clientRegistration.getClientId());
51+
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
52+
headers.setBasicAuth(clientId, clientSecret);
4853
}
4954
return headers;
5055
}
5156

57+
private static String encodeClientCredential(String clientCredential) {
58+
try {
59+
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
60+
}
61+
catch (UnsupportedEncodingException ex) {
62+
// Will not happen since UTF-8 is a standard charset
63+
throw new IllegalArgumentException(ex);
64+
}
65+
}
66+
5267
private static HttpHeaders getDefaultTokenRequestHeaders() {
5368
HttpHeaders headers = new HttpHeaders();
5469
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
*/
1616
package org.springframework.security.oauth2.client.endpoint;
1717

18+
import java.io.UnsupportedEncodingException;
19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
22+
import reactor.core.publisher.Mono;
23+
1824
import org.springframework.http.MediaType;
1925
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2026
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -24,10 +30,9 @@
2430
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
2531
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
2632
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
33+
import org.springframework.util.Assert;
2734
import org.springframework.web.reactive.function.BodyInserters;
2835
import org.springframework.web.reactive.function.client.WebClient;
29-
import org.springframework.util.Assert;
30-
import reactor.core.publisher.Mono;
3136

3237
import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse;
3338

@@ -74,7 +79,9 @@ public Mono<OAuth2AccessTokenResponse> getTokenResponse(OAuth2AuthorizationCodeG
7479
.accept(MediaType.APPLICATION_JSON)
7580
.headers(headers -> {
7681
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
77-
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
82+
String clientId = encodeClientCredential(clientRegistration.getClientId());
83+
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
84+
headers.setBasicAuth(clientId, clientSecret);
7885
}
7986
})
8087
.body(body)
@@ -91,6 +98,16 @@ public Mono<OAuth2AccessTokenResponse> getTokenResponse(OAuth2AuthorizationCodeG
9198
});
9299
}
93100

101+
private static String encodeClientCredential(String clientCredential) {
102+
try {
103+
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
104+
}
105+
catch (UnsupportedEncodingException ex) {
106+
// Will not happen since UTF-8 is a standard charset
107+
throw new IllegalArgumentException(ex);
108+
}
109+
}
110+
94111
private static BodyInserters.FormInserter<String> body(OAuth2AuthorizationExchange authorizationExchange, ClientRegistration clientRegistration) {
95112
OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse();
96113
BodyInserters.FormInserter<String> body = BodyInserters

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
*/
1616
package org.springframework.security.oauth2.client.endpoint;
1717

18+
import java.io.UnsupportedEncodingException;
19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Set;
22+
import java.util.function.Consumer;
23+
24+
import reactor.core.publisher.Mono;
25+
1826
import org.springframework.core.io.buffer.DataBuffer;
1927
import org.springframework.core.io.buffer.DataBufferUtils;
2028
import org.springframework.http.HttpHeaders;
@@ -30,10 +38,6 @@
3038
import org.springframework.web.reactive.function.BodyInserters;
3139
import org.springframework.web.reactive.function.client.WebClient;
3240
import org.springframework.web.reactive.function.client.WebClientResponseException;
33-
import reactor.core.publisher.Mono;
34-
35-
import java.util.Set;
36-
import java.util.function.Consumer;
3741

3842
import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse;
3943

@@ -98,11 +102,23 @@ private Consumer<HttpHeaders> headers(ClientRegistration clientRegistration) {
98102
return headers -> {
99103
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
100104
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
101-
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
105+
String clientId = encodeClientCredential(clientRegistration.getClientId());
106+
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
107+
headers.setBasicAuth(clientId, clientSecret);
102108
}
103109
};
104110
}
105111

112+
private static String encodeClientCredential(String clientCredential) {
113+
try {
114+
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
115+
}
116+
catch (UnsupportedEncodingException ex) {
117+
// Will not happen since UTF-8 is a standard charset
118+
throw new IllegalArgumentException(ex);
119+
}
120+
}
121+
106122
private static BodyInserters.FormInserter<String> body(OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
107123
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
108124
BodyInserters.FormInserter<String> body = BodyInserters

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
*/
1616
package org.springframework.security.oauth2.client.endpoint;
1717

18+
import java.io.UnsupportedEncodingException;
19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Collections;
22+
import java.util.function.Consumer;
23+
24+
import reactor.core.publisher.Mono;
25+
1826
import org.springframework.core.io.buffer.DataBuffer;
1927
import org.springframework.core.io.buffer.DataBufferUtils;
2028
import org.springframework.http.HttpHeaders;
@@ -32,10 +40,6 @@
3240
import org.springframework.util.StringUtils;
3341
import org.springframework.web.reactive.function.BodyInserters;
3442
import org.springframework.web.reactive.function.client.WebClient;
35-
import reactor.core.publisher.Mono;
36-
37-
import java.util.Collections;
38-
import java.util.function.Consumer;
3943

4044
import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse;
4145

@@ -100,11 +104,23 @@ private static Consumer<HttpHeaders> tokenRequestHeaders(ClientRegistration clie
100104
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
101105
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
102106
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
103-
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
107+
String clientId = encodeClientCredential(clientRegistration.getClientId());
108+
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
109+
headers.setBasicAuth(clientId, clientSecret);
104110
}
105111
};
106112
}
107113

114+
private static String encodeClientCredential(String clientCredential) {
115+
try {
116+
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
117+
}
118+
catch (UnsupportedEncodingException ex) {
119+
// Will not happen since UTF-8 is a standard charset
120+
throw new IllegalArgumentException(ex);
121+
}
122+
}
123+
108124
private static BodyInserters.FormInserter<String> tokenRequestBody(OAuth2PasswordGrantRequest passwordGrantRequest) {
109125
ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
110126
BodyInserters.FormInserter<String> body = BodyInserters.fromFormData(

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
*/
1616
package org.springframework.security.oauth2.client.endpoint;
1717

18+
import java.io.UnsupportedEncodingException;
19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Collections;
22+
import java.util.function.Consumer;
23+
24+
import reactor.core.publisher.Mono;
25+
1826
import org.springframework.core.io.buffer.DataBuffer;
1927
import org.springframework.core.io.buffer.DataBufferUtils;
2028
import org.springframework.http.HttpHeaders;
@@ -32,10 +40,6 @@
3240
import org.springframework.util.StringUtils;
3341
import org.springframework.web.reactive.function.BodyInserters;
3442
import org.springframework.web.reactive.function.client.WebClient;
35-
import reactor.core.publisher.Mono;
36-
37-
import java.util.Collections;
38-
import java.util.function.Consumer;
3943

4044
import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse;
4145

@@ -88,11 +92,23 @@ private static Consumer<HttpHeaders> tokenRequestHeaders(ClientRegistration clie
8892
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
8993
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
9094
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
91-
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
95+
String clientId = encodeClientCredential(clientRegistration.getClientId());
96+
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
97+
headers.setBasicAuth(clientId, clientSecret);
9298
}
9399
};
94100
}
95101

102+
private static String encodeClientCredential(String clientCredential) {
103+
try {
104+
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
105+
}
106+
catch (UnsupportedEncodingException ex) {
107+
// Will not happen since UTF-8 is a standard charset
108+
throw new IllegalArgumentException(ex);
109+
}
110+
}
111+
96112
private static BodyInserters.FormInserter<String> tokenRequestBody(OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
97113
ClientRegistration clientRegistration = refreshTokenGrantRequest.getClientRegistration();
98114
BodyInserters.FormInserter<String> body = BodyInserters.fromFormData(

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515
*/
1616
package org.springframework.security.oauth2.client.endpoint;
1717

18+
import java.io.UnsupportedEncodingException;
19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Base64;
22+
1823
import org.junit.Before;
1924
import org.junit.Test;
25+
2026
import org.springframework.http.HttpHeaders;
2127
import org.springframework.http.HttpMethod;
2228
import org.springframework.http.MediaType;
2329
import org.springframework.http.RequestEntity;
2430
import org.springframework.security.oauth2.client.registration.ClientRegistration;
31+
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
2532
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2633
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
2734
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@@ -74,4 +81,37 @@ public void convertWhenGrantRequestValidThenConverts() {
7481
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue());
7582
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).isEqualTo("read write");
7683
}
84+
85+
// gh-9610
86+
@SuppressWarnings("unchecked")
87+
@Test
88+
public void convertWhenSpecialCharactersThenConvertsWithEncodedClientCredentials()
89+
throws UnsupportedEncodingException {
90+
String clientCredentialWithAnsiKeyboardSpecialCharacters = "~!@#$%^&*()_+{}|:\"<>?`-=[]\\;',./ ";
91+
// @formatter:off
92+
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
93+
.clientId(clientCredentialWithAnsiKeyboardSpecialCharacters)
94+
.clientSecret(clientCredentialWithAnsiKeyboardSpecialCharacters)
95+
.build();
96+
// @formatter:on
97+
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
98+
clientRegistration);
99+
RequestEntity<?> requestEntity = this.converter.convert(clientCredentialsGrantRequest);
100+
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
101+
assertThat(requestEntity.getUrl().toASCIIString())
102+
.isEqualTo(clientRegistration.getProviderDetails().getTokenUri());
103+
HttpHeaders headers = requestEntity.getHeaders();
104+
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
105+
assertThat(headers.getContentType())
106+
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
107+
String urlEncodedClientCredential = URLEncoder.encode(clientCredentialWithAnsiKeyboardSpecialCharacters,
108+
StandardCharsets.UTF_8.toString());
109+
String clientCredentials = Base64.getEncoder().encodeToString(
110+
(urlEncodedClientCredential + ":" + urlEncodedClientCredential).getBytes(StandardCharsets.UTF_8));
111+
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic " + clientCredentials);
112+
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
113+
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
114+
.isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue());
115+
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
116+
}
77117
}

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 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,12 +16,17 @@
1616

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

19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Base64;
22+
1923
import okhttp3.mockwebserver.MockResponse;
2024
import okhttp3.mockwebserver.MockWebServer;
2125
import okhttp3.mockwebserver.RecordedRequest;
2226
import org.junit.After;
2327
import org.junit.Before;
2428
import org.junit.Test;
29+
2530
import org.springframework.http.HttpHeaders;
2631
import org.springframework.http.MediaType;
2732
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -82,6 +87,35 @@ public void getTokenResponseWhenHeaderThenSuccess() throws Exception {
8287
assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser");
8388
}
8489

90+
// gh-9610
91+
@Test
92+
public void getTokenResponseWhenSpecialCharactersThenSuccessWithEncodedClientCredentials() throws Exception {
93+
// @formatter:off
94+
enqueueJson("{\n"
95+
+ " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n"
96+
+ " \"token_type\":\"bearer\",\n"
97+
+ " \"expires_in\":3600,\n"
98+
+ " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n"
99+
+ " \"scope\":\"create\"\n"
100+
+ "}");
101+
// @formatter:on
102+
String clientCredentialWithAnsiKeyboardSpecialCharacters = "~!@#$%^&*()_+{}|:\"<>?`-=[]\\;',./ ";
103+
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(
104+
this.clientRegistration.clientId(clientCredentialWithAnsiKeyboardSpecialCharacters)
105+
.clientSecret(clientCredentialWithAnsiKeyboardSpecialCharacters).build());
106+
OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block();
107+
RecordedRequest actualRequest = this.server.takeRequest();
108+
String body = actualRequest.getBody().readUtf8();
109+
assertThat(response.getAccessToken()).isNotNull();
110+
String urlEncodedClientCredentialecret = URLEncoder.encode(clientCredentialWithAnsiKeyboardSpecialCharacters,
111+
StandardCharsets.UTF_8.toString());
112+
String clientCredentials = Base64.getEncoder()
113+
.encodeToString((urlEncodedClientCredentialecret + ":" + urlEncodedClientCredentialecret)
114+
.getBytes(StandardCharsets.UTF_8));
115+
assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic " + clientCredentials);
116+
assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser");
117+
}
118+
85119
@Test
86120
public void getTokenResponseWhenPostThenSuccess() throws Exception {
87121
ClientRegistration registration = this.clientRegistration

0 commit comments

Comments
 (0)