Skip to content

Commit c1f2f7d

Browse files
Encode clientId and clientSecret
Closes gh-15988
1 parent fa58ebb commit c1f2f7d

File tree

4 files changed

+283
-0
lines changed

4 files changed

+283
-0
lines changed

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

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

1919
import java.io.Serial;
2020
import java.net.URI;
21+
import java.net.URLEncoder;
22+
import java.nio.charset.Charset;
23+
import java.nio.charset.StandardCharsets;
2124
import java.time.Instant;
2225
import java.util.ArrayList;
2326
import java.util.Arrays;
@@ -79,7 +82,9 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
7982
* @param introspectionUri The introspection endpoint uri
8083
* @param clientId The client id authorized to introspect
8184
* @param clientSecret The client's secret
85+
* @deprecated
8286
*/
87+
@Deprecated(since = "6.5", forRemoval = true)
8388
public SpringOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
8489
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
8590
Assert.notNull(clientId, "clientId cannot be null");
@@ -295,4 +300,94 @@ default List<String> getScopes() {
295300

296301
}
297302

303+
/**
304+
* Used to build {@link SpringOpaqueTokenIntrospector}.
305+
*
306+
* @author Ngoc Nhan
307+
* @since 6.5
308+
*/
309+
public static final class SpringOpaqueTokenIntrospectorBuilder {
310+
311+
private final String introspectionUri;
312+
313+
private SpringOpaqueTokenIntrospectorBuilder(String introspectionUri) {
314+
this.introspectionUri = introspectionUri;
315+
}
316+
317+
/**
318+
* Creates a {@code SpringOpaqueTokenIntrospectorBuilder} with the provided
319+
* parameters
320+
* @param introspectionUri The introspection endpoint uri
321+
* @return the {@link SpringOpaqueTokenIntrospectorBuilder}
322+
* @since 6.5
323+
*/
324+
public static SpringOpaqueTokenIntrospectorBuilder withIntrospectionUri(String introspectionUri) {
325+
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
326+
return new SpringOpaqueTokenIntrospectorBuilder(introspectionUri);
327+
}
328+
329+
/**
330+
* Creates a {@code SpringOpaqueTokenIntrospector} with the provided parameters
331+
* @param clientId The client id authorized that should be encode
332+
* @param clientSecret The client secret that should be encode for the authorized
333+
* client
334+
* @return the {@link SpringOpaqueTokenIntrospector}
335+
* @since 6.5
336+
*/
337+
public SpringOpaqueTokenIntrospector introspectionEncodeClientCredentials(String clientId,
338+
String clientSecret) {
339+
return this.introspectionEncodeClientCredentials(clientId, clientSecret, StandardCharsets.UTF_8);
340+
}
341+
342+
/**
343+
* Creates a {@code SpringOpaqueTokenIntrospector} with the provided parameters
344+
* @param clientId The client id authorized that should be encode
345+
* @param clientSecret The client secret that should be encode for the authorized
346+
* client
347+
* @param charset the charset to use
348+
* @return the {@link SpringOpaqueTokenIntrospector}
349+
* @since 6.5
350+
*/
351+
public SpringOpaqueTokenIntrospector introspectionEncodeClientCredentials(String clientId, String clientSecret,
352+
Charset charset) {
353+
Assert.notNull(clientId, "clientId cannot be null");
354+
Assert.notNull(clientSecret, "clientSecret cannot be null");
355+
Assert.notNull(charset, "charset cannot be null");
356+
String encodeClientId = URLEncoder.encode(clientId, charset);
357+
String encodeClientSecret = URLEncoder.encode(clientSecret, charset);
358+
RestTemplate restTemplate = new RestTemplate();
359+
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(encodeClientId, encodeClientSecret));
360+
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate);
361+
}
362+
363+
/**
364+
* Creates a {@code SpringOpaqueTokenIntrospector} with the provided parameters
365+
* @param clientId The client id authorized
366+
* @param clientSecret The client secret for the authorized client
367+
* @return the {@link SpringOpaqueTokenIntrospector}
368+
* @since 6.5
369+
*/
370+
public SpringOpaqueTokenIntrospector introspectionClientCredentials(String clientId, String clientSecret) {
371+
Assert.notNull(clientId, "clientId cannot be null");
372+
Assert.notNull(clientSecret, "clientSecret cannot be null");
373+
RestTemplate restTemplate = new RestTemplate();
374+
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
375+
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate);
376+
}
377+
378+
/**
379+
* Creates a {@code SpringOpaqueTokenIntrospector} with the provided parameters
380+
* The given {@link RestOperations} should perform its own client authentication
381+
* against the introspection endpoint.
382+
* @param restOperations The client for performing the introspection request
383+
* @return the {@link SpringOpaqueTokenIntrospector}
384+
* @since 6.5
385+
*/
386+
public SpringOpaqueTokenIntrospector withRestOperations(RestOperations restOperations) {
387+
Assert.notNull(restOperations, "restOperations cannot be null");
388+
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restOperations);
389+
}
390+
391+
}
392+
298393
}

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,9 @@
1818

1919
import java.io.Serial;
2020
import java.net.URI;
21+
import java.net.URLEncoder;
22+
import java.nio.charset.Charset;
23+
import java.nio.charset.StandardCharsets;
2124
import java.time.Instant;
2225
import java.util.ArrayList;
2326
import java.util.Arrays;
@@ -74,7 +77,9 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
7477
* @param introspectionUri The introspection endpoint uri
7578
* @param clientId The client id authorized to introspect
7679
* @param clientSecret The client secret for the authorized client
80+
* @deprecated
7781
*/
82+
@Deprecated(since = "6.5", forRemoval = true)
7883
public SpringReactiveOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
7984
Assert.hasText(introspectionUri, "introspectionUri cannot be empty");
8085
Assert.hasText(clientId, "clientId cannot be empty");
@@ -249,4 +254,99 @@ default List<String> getScopes() {
249254

250255
}
251256

257+
/**
258+
* Used to build {@link SpringReactiveOpaqueTokenIntrospector}.
259+
*
260+
* @author Ngoc Nhan
261+
* @since 6.5
262+
*/
263+
public static final class SpringReactiveOpaqueTokenIntrospectorBuilder {
264+
265+
private final String introspectionUri;
266+
267+
private SpringReactiveOpaqueTokenIntrospectorBuilder(String introspectionUri) {
268+
this.introspectionUri = introspectionUri;
269+
}
270+
271+
/**
272+
* Creates a {@code SpringReactiveOpaqueTokenIntrospectorBuilder} with the
273+
* provided parameters
274+
* @param introspectionUri The introspection endpoint uri
275+
* @return the {@link SpringReactiveOpaqueTokenIntrospectorBuilder}
276+
* @since 6.5
277+
*/
278+
public static SpringReactiveOpaqueTokenIntrospectorBuilder withIntrospectionUri(String introspectionUri) {
279+
280+
return new SpringReactiveOpaqueTokenIntrospectorBuilder(introspectionUri);
281+
}
282+
283+
/**
284+
* Creates a {@code SpringReactiveOpaqueTokenIntrospector} with the provided
285+
* parameters
286+
* @param clientId The client id authorized that should be encode
287+
* @param clientSecret The client secret that should be encode for the authorized
288+
* client
289+
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
290+
* @since 6.5
291+
*/
292+
public SpringReactiveOpaqueTokenIntrospector introspectionEncodeClientCredentials(String clientId,
293+
String clientSecret) {
294+
return this.introspectionEncodeClientCredentials(clientId, clientSecret, StandardCharsets.UTF_8);
295+
}
296+
297+
/**
298+
* Creates a {@code SpringReactiveOpaqueTokenIntrospector} with the provided
299+
* parameters
300+
* @param clientId The client id authorized that should be encode
301+
* @param clientSecret The client secret that should be encode for the authorized
302+
* client
303+
* @param charset the charset to use
304+
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
305+
* @since 6.5
306+
*/
307+
public SpringReactiveOpaqueTokenIntrospector introspectionEncodeClientCredentials(String clientId,
308+
String clientSecret, Charset charset) {
309+
Assert.notNull(clientId, "clientId cannot be null");
310+
Assert.notNull(clientSecret, "clientSecret cannot be null");
311+
Assert.notNull(charset, "charset cannot be null");
312+
String encodeClientId = URLEncoder.encode(clientId, charset);
313+
String encodeClientSecret = URLEncoder.encode(clientSecret, charset);
314+
WebClient webClient = WebClient.builder()
315+
.defaultHeaders((h) -> h.setBasicAuth(encodeClientId, encodeClientSecret))
316+
.build();
317+
return new SpringReactiveOpaqueTokenIntrospector(this.introspectionUri, webClient);
318+
}
319+
320+
/**
321+
* Creates a {@code SpringReactiveOpaqueTokenIntrospector} with the provided
322+
* parameters
323+
* @param clientId The client id authorized
324+
* @param clientSecret The client secret for the authorized client
325+
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
326+
* @since 6.5
327+
*/
328+
public SpringReactiveOpaqueTokenIntrospector introspectionClientCredentials(String clientId,
329+
String clientSecret) {
330+
Assert.notNull(clientId, "clientId cannot be null");
331+
Assert.notNull(clientSecret, "clientSecret cannot be null");
332+
WebClient webClient = WebClient.builder()
333+
.defaultHeaders((h) -> h.setBasicAuth(clientId, clientSecret))
334+
.build();
335+
return new SpringReactiveOpaqueTokenIntrospector(this.introspectionUri, webClient);
336+
}
337+
338+
/**
339+
* Creates a {@code SpringReactiveOpaqueTokenIntrospector} with the provided
340+
* parameters
341+
* @param webClient The client for performing the introspection request
342+
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
343+
* @since 6.5
344+
*/
345+
public SpringReactiveOpaqueTokenIntrospector withRestOperations(WebClient webClient) {
346+
Assert.notNull(webClient, "webClient cannot be null");
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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
4444
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
4545
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
46+
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector.SpringOpaqueTokenIntrospectorBuilder;
4647
import org.springframework.web.client.RestOperations;
4748

4849
import static org.assertj.core.api.Assertions.assertThat;
@@ -339,6 +340,48 @@ 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 = SpringOpaqueTokenIntrospectorBuilder
373+
.withIntrospectionUri(introspectUri)
374+
.introspectionEncodeClientCredentials("client%&1", "secret@$2");
375+
OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token");
376+
// @formatter:off
377+
assertThat(authority.getAttributes())
378+
.isNotNull()
379+
.containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true)
380+
.containsEntry(OAuth2TokenIntrospectionClaimNames.USERNAME, "client%&1");
381+
// @formatter:on
382+
}
383+
}
384+
342385
private static ResponseEntity<Map<String, Object>> response(String content) {
343386
HttpHeaders headers = new HttpHeaders();
344387
headers.setContentType(MediaType.APPLICATION_JSON);

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
4343
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
4444
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
45+
import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector.SpringReactiveOpaqueTokenIntrospectorBuilder;
4546
import org.springframework.web.reactive.function.client.ClientResponse;
4647
import org.springframework.web.reactive.function.client.WebClient;
4748

@@ -261,6 +262,50 @@ 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 = SpringReactiveOpaqueTokenIntrospectorBuilder
297+
.withIntrospectionUri(introspectUri)
298+
.introspectionEncodeClientCredentials("client%&1", "secret@$2");
299+
OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token").block();
300+
// @formatter:off
301+
assertThat(authority.getAttributes())
302+
.isNotNull()
303+
.containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true)
304+
.containsEntry(OAuth2TokenIntrospectionClaimNames.USERNAME, "client%&1");
305+
// @formatter:on
306+
}
307+
}
308+
264309
private WebClient mockResponse(String response) {
265310
return mockResponse(toMap(response));
266311
}

0 commit comments

Comments
 (0)