Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encode clientId and clientSecret for OpaqueTokenIntrospector and ReactiveOpaqueTokenIntrospector #16008

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,6 +18,8 @@

import java.io.Serial;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -77,9 +79,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
/**
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
* @param introspectionUri The introspection endpoint uri
* @param clientId The client id authorized to introspect
* @param clientSecret The client's secret
* @param clientId The URL-encoded client id authorized to introspect
* @param clientSecret The URL-encoded client secret authorized to introspect
* @deprecated Please use {@link SpringOpaqueTokenIntrospector.Builder}
*/
@Deprecated(since = "6.5", forRemoval = true)
public SpringOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
Assert.notNull(clientId, "clientId cannot be null");
Expand Down Expand Up @@ -269,6 +273,18 @@ private Collection<GrantedAuthority> authorities(List<String> scopes) {
return authorities;
}

/**
* Creates a {@code SpringOpaqueTokenIntrospector.Builder} with the given
* introspection endpoint uri
* @param introspectionUri The introspection endpoint uri
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
* @since 6.5
*/
public static Builder withIntrospectionUri(String introspectionUri) {
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
return new Builder(introspectionUri);
}

// gh-7563
private static final class ArrayListFromString extends ArrayList<String> {

Expand All @@ -295,4 +311,61 @@ default List<String> getScopes() {

}

/**
* Used to build {@link SpringOpaqueTokenIntrospector}.
*
* @author Ngoc Nhan
* @since 6.5
*/
public static final class Builder {

private final String introspectionUri;

private String clientId;

private String clientSecret;

private Builder(String introspectionUri) {
this.introspectionUri = introspectionUri;
}

/**
* The builder will {@link URLEncoder encode} the client id that you provide, so
* please give the unencoded value.
* @param clientId The unencoded client id
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
* @since 6.5
*/
public Builder clientId(String clientId) {
jzheaux marked this conversation as resolved.
Show resolved Hide resolved
Assert.notNull(clientId, "clientId cannot be null");
this.clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8);
return this;
}

/**
* The builder will {@link URLEncoder encode} the client secret that you provide,
* so please give the unencoded value.
* @param clientSecret The unencoded client secret
* @return the {@link SpringOpaqueTokenIntrospector.Builder}
* @since 6.5
*/
public Builder clientSecret(String clientSecret) {
Assert.notNull(clientSecret, "clientSecret cannot be null");
this.clientSecret = URLEncoder.encode(clientSecret, StandardCharsets.UTF_8);
return this;
}

/**
* Creates a {@code SpringOpaqueTokenIntrospector}
* @return the {@link SpringOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringOpaqueTokenIntrospector build() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(this.clientId, this.clientSecret));
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate);
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,6 +18,8 @@

import java.io.Serial;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -72,9 +74,11 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
* Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
* parameters
* @param introspectionUri The introspection endpoint uri
* @param clientId The client id authorized to introspect
* @param clientSecret The client secret for the authorized client
* @param clientId The URL-encoded client id authorized to introspect
* @param clientSecret The URL-encoded client secret authorized to introspect
* @deprecated Please use {@link SpringReactiveOpaqueTokenIntrospector.Builder}
*/
@Deprecated(since = "6.5", forRemoval = true)
public SpringReactiveOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
Assert.hasText(introspectionUri, "introspectionUri cannot be empty");
Assert.hasText(clientId, "clientId cannot be empty");
Expand Down Expand Up @@ -223,6 +227,18 @@ private Collection<GrantedAuthority> authorities(List<String> scopes) {
return authorities;
}

/**
* Creates a {@code SpringReactiveOpaqueTokenIntrospector.Builder} with the given
* introspection endpoint uri
* @param introspectionUri The introspection endpoint uri
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
* @since 6.5
*/
public static Builder withIntrospectionUri(String introspectionUri) {

return new Builder(introspectionUri);
}

// gh-7563
private static final class ArrayListFromString extends ArrayList<String> {

Expand All @@ -249,4 +265,62 @@ default List<String> getScopes() {

}

/**
* Used to build {@link SpringReactiveOpaqueTokenIntrospector}.
*
* @author Ngoc Nhan
* @since 6.5
*/
public static final class Builder {

private final String introspectionUri;

private String clientId;

private String clientSecret;

private Builder(String introspectionUri) {
this.introspectionUri = introspectionUri;
}

/**
* The builder will {@link URLEncoder encode} the client id that you provide, so
* please give the unencoded value.
* @param clientId The unencoded client id
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
* @since 6.5
*/
public Builder clientId(String clientId) {
Assert.notNull(clientId, "clientId cannot be null");
this.clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8);
return this;
}

/**
* The builder will {@link URLEncoder encode} the client secret that you provide,
* so please give the unencoded value.
* @param clientSecret The unencoded client secret
* @return the {@link SpringReactiveOpaqueTokenIntrospector.Builder}
* @since 6.5
*/
public Builder clientSecret(String clientSecret) {
Assert.notNull(clientSecret, "clientSecret cannot be null");
this.clientSecret = URLEncoder.encode(clientSecret, StandardCharsets.UTF_8);
return this;
}

/**
* Creates a {@code SpringReactiveOpaqueTokenIntrospector}
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringReactiveOpaqueTokenIntrospector build() {
WebClient webClient = WebClient.builder()
.defaultHeaders((h) -> h.setBasicAuth(this.clientId, this.clientSecret))
.build();
return new SpringReactiveOpaqueTokenIntrospector(this.introspectionUri, webClient);
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -339,6 +339,50 @@ public void setAuthenticationConverterWhenNonNullConverterGivenThenConverterUsed
verify(authenticationConverter).convert(any());
}

@Test
public void introspectWithoutEncodeClientCredentialsThenExceptionIsThrown() throws Exception {
try (MockWebServer server = new MockWebServer()) {
String response = """
{
"active": true,
"username": "client%&1"
}
""";
server.setDispatcher(requiresAuth("client%25%261", "secret%40%242", response));
String introspectUri = server.url("/introspect").toString();
OpaqueTokenIntrospector introspectionClient = new SpringOpaqueTokenIntrospector(introspectUri, "client%&1",
"secret@$2");
assertThatExceptionOfType(OAuth2IntrospectionException.class)
.isThrownBy(() -> introspectionClient.introspect("token"));
}
}

@Test
public void introspectWithEncodeClientCredentialsThenOk() throws Exception {
try (MockWebServer server = new MockWebServer()) {
String response = """
{
"active": true,
"username": "client&1"
}
""";
server.setDispatcher(requiresAuth("client%261", "secret%40%242", response));
String introspectUri = server.url("/introspect").toString();
OpaqueTokenIntrospector introspectionClient = SpringOpaqueTokenIntrospector
.withIntrospectionUri(introspectUri)
.clientId("client&1")
.clientSecret("secret@$2")
.build();
OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token");
// @formatter:off
assertThat(authority.getAttributes())
.isNotNull()
.containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true)
.containsEntry(OAuth2TokenIntrospectionClaimNames.USERNAME, "client&1");
// @formatter:on
}
}

private static ResponseEntity<Map<String, Object>> response(String content) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -261,6 +261,52 @@ public void constructorWhenRestOperationsIsNullThenIllegalArgumentException() {
.isThrownBy(() -> new SpringReactiveOpaqueTokenIntrospector(INTROSPECTION_URL, null));
}

@Test
public void introspectWithoutEncodeClientCredentialsThenExceptionIsThrown() throws Exception {
try (MockWebServer server = new MockWebServer()) {
String response = """
{
"active": true,
"username": "client%&1"
}
""";
server.setDispatcher(requiresAuth("client%25%261", "secret%40%242", response));
String introspectUri = server.url("/introspect").toString();
ReactiveOpaqueTokenIntrospector introspectionClient = new SpringReactiveOpaqueTokenIntrospector(
introspectUri, "client%&1", "secret@$2");
// @formatter:off
assertThatExceptionOfType(OAuth2IntrospectionException.class)
.isThrownBy(() -> introspectionClient.introspect("token").block());
// @formatter:on
}
}

@Test
public void introspectWithEncodeClientCredentialsThenOk() throws Exception {
try (MockWebServer server = new MockWebServer()) {
String response = """
{
"active": true,
"username": "client&1"
}
""";
server.setDispatcher(requiresAuth("client%261", "secret%40%242", response));
String introspectUri = server.url("/introspect").toString();
ReactiveOpaqueTokenIntrospector introspectionClient = SpringReactiveOpaqueTokenIntrospector
.withIntrospectionUri(introspectUri)
.clientId("client&1")
.clientSecret("secret@$2")
.build();
OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token").block();
// @formatter:off
assertThat(authority.getAttributes())
.isNotNull()
.containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true)
.containsEntry(OAuth2TokenIntrospectionClaimNames.USERNAME, "client&1");
// @formatter:on
}
}

private WebClient mockResponse(String response) {
return mockResponse(toMap(response));
}
Expand Down
Loading