Skip to content

Commit dd8cf97

Browse files
Encode clientId and clientSecret
Closes gh-15988 Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
1 parent fa58ebb commit dd8cf97

File tree

4 files changed

+291
-0
lines changed

4 files changed

+291
-0
lines changed

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.io.Serial;
2020
import java.net.URI;
21+
import java.net.URLEncoder;
22+
import java.nio.charset.Charset;
2123
import java.time.Instant;
2224
import java.util.ArrayList;
2325
import java.util.Arrays;
@@ -79,7 +81,9 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
7981
* @param introspectionUri The introspection endpoint uri
8082
* @param clientId The client id authorized to introspect
8183
* @param clientSecret The client's secret
84+
* @deprecated Please use {@link SpringOpaqueTokenIntrospector.Builder}
8285
*/
86+
@Deprecated(since = "6.5", forRemoval = true)
8387
public SpringOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
8488
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
8589
Assert.notNull(clientId, "clientId cannot be null");
@@ -269,6 +273,18 @@ private Collection<GrantedAuthority> authorities(List<String> scopes) {
269273
return authorities;
270274
}
271275

276+
/**
277+
* Creates a {@code SpringOpaqueTokenIntrospector.Builder} with the given
278+
* introspection endpoint uri
279+
* @param introspectionUri The introspection endpoint uri
280+
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
281+
* @since 6.5
282+
*/
283+
public static Builder withIntrospectionUri(String introspectionUri) {
284+
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
285+
return new Builder(introspectionUri);
286+
}
287+
272288
// gh-7563
273289
private static final class ArrayListFromString extends ArrayList<String> {
274290

@@ -295,4 +311,87 @@ default List<String> getScopes() {
295311

296312
}
297313

314+
/**
315+
* Used to build {@link SpringOpaqueTokenIntrospector}.
316+
*
317+
* @author Ngoc Nhan
318+
* @since 6.5
319+
*/
320+
public static final class Builder {
321+
322+
private final String introspectionUri;
323+
324+
private String clientId;
325+
326+
private String clientSecret;
327+
328+
private Builder(String introspectionUri) {
329+
this.introspectionUri = introspectionUri;
330+
}
331+
332+
/**
333+
* Uses the given parameters to build {@code SpringOpaqueTokenIntrospector}
334+
* @param clientId The client id authorized that should be encoded
335+
* @param charset The charset to use
336+
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
337+
* @since 6.5
338+
*/
339+
public Builder clientId(String clientId, Charset charset) {
340+
Assert.notNull(clientId, "clientId cannot be null");
341+
Assert.notNull(charset, "charset cannot be null");
342+
this.clientId = URLEncoder.encode(clientId, charset);
343+
return this;
344+
}
345+
346+
/**
347+
* Uses the given parameter to build {@code SpringOpaqueTokenIntrospector}
348+
* @param clientId The client id authorized
349+
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
350+
* @since 6.5
351+
*/
352+
public Builder clientId(String clientId) {
353+
Assert.notNull(clientId, "clientId cannot be null");
354+
this.clientId = clientId;
355+
return this;
356+
}
357+
358+
/**
359+
* Uses the given parameters to build {@code SpringOpaqueTokenIntrospector}
360+
* @param clientSecret The client's secret that should be encoded
361+
* @param charset The charset to use
362+
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
363+
* @since 6.5
364+
*/
365+
public Builder clientSecret(String clientSecret, Charset charset) {
366+
Assert.notNull(clientSecret, "clientSecret cannot be null");
367+
Assert.notNull(charset, "charset cannot be null");
368+
this.clientSecret = URLEncoder.encode(clientSecret, charset);
369+
return this;
370+
}
371+
372+
/**
373+
* Uses the given parameter to build {@code SpringOpaqueTokenIntrospector}
374+
* @param clientSecret The client's secret
375+
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
376+
* @since 6.5
377+
*/
378+
public Builder clientSecret(String clientSecret) {
379+
Assert.notNull(clientSecret, "clientSecret cannot be null");
380+
this.clientSecret = clientSecret;
381+
return this;
382+
}
383+
384+
/**
385+
* Creates a {@code SpringOpaqueTokenIntrospector}
386+
* @return the {@link SpringOpaqueTokenIntrospector}
387+
* @since 6.5
388+
*/
389+
public SpringOpaqueTokenIntrospector build() {
390+
RestTemplate restTemplate = new RestTemplate();
391+
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(this.clientId, this.clientSecret));
392+
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate);
393+
}
394+
395+
}
396+
298397
}

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.io.Serial;
2020
import java.net.URI;
21+
import java.net.URLEncoder;
22+
import java.nio.charset.Charset;
2123
import java.time.Instant;
2224
import java.util.ArrayList;
2325
import java.util.Arrays;
@@ -74,7 +76,9 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
7476
* @param introspectionUri The introspection endpoint uri
7577
* @param clientId The client id authorized to introspect
7678
* @param clientSecret The client secret for the authorized client
79+
* @deprecated Please use {@link SpringReactiveOpaqueTokenIntrospector.Builder}
7780
*/
81+
@Deprecated(since = "6.5", forRemoval = true)
7882
public SpringReactiveOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
7983
Assert.hasText(introspectionUri, "introspectionUri cannot be empty");
8084
Assert.hasText(clientId, "clientId cannot be empty");
@@ -223,6 +227,18 @@ private Collection<GrantedAuthority> authorities(List<String> scopes) {
223227
return authorities;
224228
}
225229

230+
/**
231+
* Creates a {@code SpringReactiveOpaqueTokenIntrospector.Builder} with the given
232+
* introspection endpoint uri
233+
* @param introspectionUri The introspection endpoint uri
234+
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
235+
* @since 6.5
236+
*/
237+
public static Builder withIntrospectionUri(String introspectionUri) {
238+
239+
return new Builder(introspectionUri);
240+
}
241+
226242
// gh-7563
227243
private static final class ArrayListFromString extends ArrayList<String> {
228244

@@ -249,4 +265,88 @@ default List<String> getScopes() {
249265

250266
}
251267

268+
/**
269+
* Used to build {@link SpringReactiveOpaqueTokenIntrospector}.
270+
*
271+
* @author Ngoc Nhan
272+
* @since 6.5
273+
*/
274+
public static final class Builder {
275+
276+
private final String introspectionUri;
277+
278+
private String clientId;
279+
280+
private String clientSecret;
281+
282+
private Builder(String introspectionUri) {
283+
this.introspectionUri = introspectionUri;
284+
}
285+
286+
/**
287+
* Uses the given parameters to build {@code SpringOpaqueTokenIntrospector}
288+
* @param clientId The client id authorized that should be encoded
289+
* @param charset The charset to use
290+
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
291+
* @since 6.5
292+
*/
293+
public Builder clientId(String clientId, Charset charset) {
294+
Assert.notNull(clientId, "clientId cannot be null");
295+
Assert.notNull(charset, "charset cannot be null");
296+
this.clientId = URLEncoder.encode(clientId, charset);
297+
return this;
298+
}
299+
300+
/**
301+
* Uses the given parameter to build {@code SpringOpaqueTokenIntrospector}
302+
* @param clientId The client id authorized
303+
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
304+
* @since 6.5
305+
*/
306+
public Builder clientId(String clientId) {
307+
Assert.notNull(clientId, "clientId cannot be null");
308+
this.clientId = clientId;
309+
return this;
310+
}
311+
312+
/**
313+
* Uses the given parameters to build {@code SpringOpaqueTokenIntrospector}
314+
* @param clientSecret The client's secret that should be encoded
315+
* @param charset The charset to use
316+
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
317+
* @since 6.5
318+
*/
319+
public Builder clientSecret(String clientSecret, Charset charset) {
320+
Assert.notNull(clientSecret, "clientSecret cannot be null");
321+
Assert.notNull(charset, "charset cannot be null");
322+
this.clientSecret = URLEncoder.encode(clientSecret, charset);
323+
return this;
324+
}
325+
326+
/**
327+
* Uses the given parameter to build {@code SpringOpaqueTokenIntrospector}
328+
* @param clientSecret The client's secret
329+
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
330+
* @since 6.5
331+
*/
332+
public Builder clientSecret(String clientSecret) {
333+
Assert.notNull(clientSecret, "clientSecret cannot be null");
334+
this.clientSecret = clientSecret;
335+
return this;
336+
}
337+
338+
/**
339+
* Creates a {@code SpringReactiveOpaqueTokenIntrospector}
340+
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
341+
* @since 6.5
342+
*/
343+
public SpringReactiveOpaqueTokenIntrospector build() {
344+
WebClient webClient = WebClient.builder()
345+
.defaultHeaders((h) -> h.setBasicAuth(this.clientId, this.clientSecret))
346+
.build();
347+
return new SpringReactiveOpaqueTokenIntrospector(this.introspectionUri, webClient);
348+
}
349+
350+
}
351+
252352
}

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospectorTests.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.security.oauth2.server.resource.introspection;
1818

1919
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
2021
import java.time.Instant;
2122
import java.util.Arrays;
2223
import java.util.Base64;
@@ -339,6 +340,50 @@ public void setAuthenticationConverterWhenNonNullConverterGivenThenConverterUsed
339340
verify(authenticationConverter).convert(any());
340341
}
341342

343+
@Test
344+
public void introspectWithoutEncodeClientCredentialsThenExceptionIsThrown() throws Exception {
345+
try (MockWebServer server = new MockWebServer()) {
346+
String response = """
347+
{
348+
"active": true,
349+
"username": "client%&1"
350+
}
351+
""";
352+
server.setDispatcher(requiresAuth("client%25%261", "secret%40%242", response));
353+
String introspectUri = server.url("/introspect").toString();
354+
OpaqueTokenIntrospector introspectionClient = new SpringOpaqueTokenIntrospector(introspectUri, "client%&1",
355+
"secret@$2");
356+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
357+
.isThrownBy(() -> introspectionClient.introspect("token"));
358+
}
359+
}
360+
361+
@Test
362+
public void introspectWithEncodeClientCredentialsThenOk() throws Exception {
363+
try (MockWebServer server = new MockWebServer()) {
364+
String response = """
365+
{
366+
"active": true,
367+
"username": "client%&1"
368+
}
369+
""";
370+
server.setDispatcher(requiresAuth("client%25%261", "secret%40%242", response));
371+
String introspectUri = server.url("/introspect").toString();
372+
OpaqueTokenIntrospector introspectionClient = SpringOpaqueTokenIntrospector
373+
.withIntrospectionUri(introspectUri)
374+
.clientId("client%&1", StandardCharsets.UTF_8)
375+
.clientSecret("secret@$2", StandardCharsets.UTF_8)
376+
.build();
377+
OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token");
378+
// @formatter:off
379+
assertThat(authority.getAttributes())
380+
.isNotNull()
381+
.containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true)
382+
.containsEntry(OAuth2TokenIntrospectionClaimNames.USERNAME, "client%&1");
383+
// @formatter:on
384+
}
385+
}
386+
342387
private static ResponseEntity<Map<String, Object>> response(String content) {
343388
HttpHeaders headers = new HttpHeaders();
344389
headers.setContentType(MediaType.APPLICATION_JSON);

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.security.oauth2.server.resource.introspection;
1818

1919
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
2021
import java.time.Instant;
2122
import java.util.Arrays;
2223
import java.util.Base64;
@@ -261,6 +262,52 @@ public void constructorWhenRestOperationsIsNullThenIllegalArgumentException() {
261262
.isThrownBy(() -> new SpringReactiveOpaqueTokenIntrospector(INTROSPECTION_URL, null));
262263
}
263264

265+
@Test
266+
public void introspectWithoutEncodeClientCredentialsThenExceptionIsThrown() throws Exception {
267+
try (MockWebServer server = new MockWebServer()) {
268+
String response = """
269+
{
270+
"active": true,
271+
"username": "client%&1"
272+
}
273+
""";
274+
server.setDispatcher(requiresAuth("client%25%261", "secret%40%242", response));
275+
String introspectUri = server.url("/introspect").toString();
276+
ReactiveOpaqueTokenIntrospector introspectionClient = new SpringReactiveOpaqueTokenIntrospector(
277+
introspectUri, "client%&1", "secret@$2");
278+
// @formatter:off
279+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
280+
.isThrownBy(() -> introspectionClient.introspect("token").block());
281+
// @formatter:on
282+
}
283+
}
284+
285+
@Test
286+
public void introspectWithEncodeClientCredentialsThenOk() throws Exception {
287+
try (MockWebServer server = new MockWebServer()) {
288+
String response = """
289+
{
290+
"active": true,
291+
"username": "client%&1"
292+
}
293+
""";
294+
server.setDispatcher(requiresAuth("client%25%261", "secret%40%242", response));
295+
String introspectUri = server.url("/introspect").toString();
296+
ReactiveOpaqueTokenIntrospector introspectionClient = SpringReactiveOpaqueTokenIntrospector
297+
.withIntrospectionUri(introspectUri)
298+
.clientId("client%&1", StandardCharsets.UTF_8)
299+
.clientSecret("secret@$2", StandardCharsets.UTF_8)
300+
.build();
301+
OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token").block();
302+
// @formatter:off
303+
assertThat(authority.getAttributes())
304+
.isNotNull()
305+
.containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true)
306+
.containsEntry(OAuth2TokenIntrospectionClaimNames.USERNAME, "client%&1");
307+
// @formatter:on
308+
}
309+
}
310+
264311
private WebClient mockResponse(String response) {
265312
return mockResponse(toMap(response));
266313
}

0 commit comments

Comments
 (0)