Skip to content

Commit aced3bc

Browse files
ngocnhan-tran1996jzheaux
authored andcommitted
Encode Introspection clientId and clientSecret
Closes gh-15988 Signed-off-by: Tran Ngoc Nhan <[email protected]>
1 parent 7c4448c commit aced3bc

File tree

4 files changed

+245
-8
lines changed

4 files changed

+245
-8
lines changed

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

+76-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -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.StandardCharsets;
2123
import java.time.Instant;
2224
import java.util.ArrayList;
2325
import java.util.Arrays;
@@ -77,9 +79,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
7779
/**
7880
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
7981
* @param introspectionUri The introspection endpoint uri
80-
* @param clientId The client id authorized to introspect
81-
* @param clientSecret The client's secret
82+
* @param clientId The URL-encoded client id authorized to introspect
83+
* @param clientSecret The URL-encoded client secret authorized to introspect
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,61 @@ 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+
* The builder will {@link URLEncoder encode} the client id that you provide, so
334+
* please give the unencoded value.
335+
* @param clientId The unencoded client id
336+
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
337+
* @since 6.5
338+
*/
339+
public Builder clientId(String clientId) {
340+
Assert.notNull(clientId, "clientId cannot be null");
341+
this.clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8);
342+
return this;
343+
}
344+
345+
/**
346+
* The builder will {@link URLEncoder encode} the client secret that you provide,
347+
* so please give the unencoded value.
348+
* @param clientSecret The unencoded client secret
349+
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
350+
* @since 6.5
351+
*/
352+
public Builder clientSecret(String clientSecret) {
353+
Assert.notNull(clientSecret, "clientSecret cannot be null");
354+
this.clientSecret = URLEncoder.encode(clientSecret, StandardCharsets.UTF_8);
355+
return this;
356+
}
357+
358+
/**
359+
* Creates a {@code SpringOpaqueTokenIntrospector}
360+
* @return the {@link SpringOpaqueTokenIntrospector}
361+
* @since 6.5
362+
*/
363+
public SpringOpaqueTokenIntrospector build() {
364+
RestTemplate restTemplate = new RestTemplate();
365+
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(this.clientId, this.clientSecret));
366+
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate);
367+
}
368+
369+
}
370+
298371
}

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

+77-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -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.StandardCharsets;
2123
import java.time.Instant;
2224
import java.util.ArrayList;
2325
import java.util.Arrays;
@@ -72,9 +74,11 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
7274
* Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
7375
* parameters
7476
* @param introspectionUri The introspection endpoint uri
75-
* @param clientId The client id authorized to introspect
76-
* @param clientSecret The client secret for the authorized client
77+
* @param clientId The URL-encoded client id authorized to introspect
78+
* @param clientSecret The URL-encoded client secret authorized to introspect
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,62 @@ 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+
* The builder will {@link URLEncoder encode} the client id that you provide, so
288+
* please give the unencoded value.
289+
* @param clientId The unencoded client id
290+
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
291+
* @since 6.5
292+
*/
293+
public Builder clientId(String clientId) {
294+
Assert.notNull(clientId, "clientId cannot be null");
295+
this.clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8);
296+
return this;
297+
}
298+
299+
/**
300+
* The builder will {@link URLEncoder encode} the client secret that you provide,
301+
* so please give the unencoded value.
302+
* @param clientSecret The unencoded client secret
303+
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
304+
* @since 6.5
305+
*/
306+
public Builder clientSecret(String clientSecret) {
307+
Assert.notNull(clientSecret, "clientSecret cannot be null");
308+
this.clientSecret = URLEncoder.encode(clientSecret, StandardCharsets.UTF_8);
309+
return this;
310+
}
311+
312+
/**
313+
* Creates a {@code SpringReactiveOpaqueTokenIntrospector}
314+
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
315+
* @since 6.5
316+
*/
317+
public SpringReactiveOpaqueTokenIntrospector build() {
318+
WebClient webClient = WebClient.builder()
319+
.defaultHeaders((h) -> h.setBasicAuth(this.clientId, this.clientSecret))
320+
.build();
321+
return new SpringReactiveOpaqueTokenIntrospector(this.introspectionUri, webClient);
322+
}
323+
324+
}
325+
252326
}

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

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -339,6 +339,50 @@ public void setAuthenticationConverterWhenNonNullConverterGivenThenConverterUsed
339339
verify(authenticationConverter).convert(any());
340340
}
341341

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

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

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -261,6 +261,52 @@ public void constructorWhenRestOperationsIsNullThenIllegalArgumentException() {
261261
.isThrownBy(() -> new SpringReactiveOpaqueTokenIntrospector(INTROSPECTION_URL, null));
262262
}
263263

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

0 commit comments

Comments
 (0)