Skip to content

Commit

Permalink
Encode clientId and clientSecret
Browse files Browse the repository at this point in the history
Closes gh-15988
  • Loading branch information
ngocnhan-tran1996 committed Dec 19, 2024
1 parent fa58ebb commit c1f2f7d
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

import java.io.Serial;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -79,7 +82,9 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
* @param introspectionUri The introspection endpoint uri
* @param clientId The client id authorized to introspect
* @param clientSecret The client's secret
* @deprecated
*/
@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 @@ -295,4 +300,94 @@ default List<String> getScopes() {

}

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

private final String introspectionUri;

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

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

/**
* Creates a {@code SpringOpaqueTokenIntrospector} with the provided parameters
* @param clientId The client id authorized that should be encode
* @param clientSecret The client secret that should be encode for the authorized
* client
* @return the {@link SpringOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringOpaqueTokenIntrospector introspectionEncodeClientCredentials(String clientId,
String clientSecret) {
return this.introspectionEncodeClientCredentials(clientId, clientSecret, StandardCharsets.UTF_8);
}

/**
* Creates a {@code SpringOpaqueTokenIntrospector} with the provided parameters
* @param clientId The client id authorized that should be encode
* @param clientSecret The client secret that should be encode for the authorized
* client
* @param charset the charset to use
* @return the {@link SpringOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringOpaqueTokenIntrospector introspectionEncodeClientCredentials(String clientId, String clientSecret,
Charset charset) {
Assert.notNull(clientId, "clientId cannot be null");
Assert.notNull(clientSecret, "clientSecret cannot be null");
Assert.notNull(charset, "charset cannot be null");
String encodeClientId = URLEncoder.encode(clientId, charset);
String encodeClientSecret = URLEncoder.encode(clientSecret, charset);
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(encodeClientId, encodeClientSecret));
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate);
}

/**
* Creates a {@code SpringOpaqueTokenIntrospector} with the provided parameters
* @param clientId The client id authorized
* @param clientSecret The client secret for the authorized client
* @return the {@link SpringOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringOpaqueTokenIntrospector introspectionClientCredentials(String clientId, String clientSecret) {
Assert.notNull(clientId, "clientId cannot be null");
Assert.notNull(clientSecret, "clientSecret cannot be null");
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate);
}

/**
* Creates a {@code SpringOpaqueTokenIntrospector} with the provided parameters
* The given {@link RestOperations} should perform its own client authentication
* against the introspection endpoint.
* @param restOperations The client for performing the introspection request
* @return the {@link SpringOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringOpaqueTokenIntrospector withRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
return new SpringOpaqueTokenIntrospector(this.introspectionUri, restOperations);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

import java.io.Serial;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -74,7 +77,9 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
* @param introspectionUri The introspection endpoint uri
* @param clientId The client id authorized to introspect
* @param clientSecret The client secret for the authorized client
* @deprecated
*/
@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 @@ -249,4 +254,99 @@ default List<String> getScopes() {

}

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

private final String introspectionUri;

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

/**
* Creates a {@code SpringReactiveOpaqueTokenIntrospectorBuilder} with the
* provided parameters
* @param introspectionUri The introspection endpoint uri
* @return the {@link SpringReactiveOpaqueTokenIntrospectorBuilder}
* @since 6.5
*/
public static SpringReactiveOpaqueTokenIntrospectorBuilder withIntrospectionUri(String introspectionUri) {

return new SpringReactiveOpaqueTokenIntrospectorBuilder(introspectionUri);
}

/**
* Creates a {@code SpringReactiveOpaqueTokenIntrospector} with the provided
* parameters
* @param clientId The client id authorized that should be encode
* @param clientSecret The client secret that should be encode for the authorized
* client
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringReactiveOpaqueTokenIntrospector introspectionEncodeClientCredentials(String clientId,
String clientSecret) {
return this.introspectionEncodeClientCredentials(clientId, clientSecret, StandardCharsets.UTF_8);
}

/**
* Creates a {@code SpringReactiveOpaqueTokenIntrospector} with the provided
* parameters
* @param clientId The client id authorized that should be encode
* @param clientSecret The client secret that should be encode for the authorized
* client
* @param charset the charset to use
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringReactiveOpaqueTokenIntrospector introspectionEncodeClientCredentials(String clientId,
String clientSecret, Charset charset) {
Assert.notNull(clientId, "clientId cannot be null");
Assert.notNull(clientSecret, "clientSecret cannot be null");
Assert.notNull(charset, "charset cannot be null");
String encodeClientId = URLEncoder.encode(clientId, charset);
String encodeClientSecret = URLEncoder.encode(clientSecret, charset);
WebClient webClient = WebClient.builder()
.defaultHeaders((h) -> h.setBasicAuth(encodeClientId, encodeClientSecret))
.build();
return new SpringReactiveOpaqueTokenIntrospector(this.introspectionUri, webClient);
}

/**
* Creates a {@code SpringReactiveOpaqueTokenIntrospector} with the provided
* parameters
* @param clientId The client id authorized
* @param clientSecret The client secret for the authorized client
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringReactiveOpaqueTokenIntrospector introspectionClientCredentials(String clientId,
String clientSecret) {
Assert.notNull(clientId, "clientId cannot be null");
Assert.notNull(clientSecret, "clientSecret cannot be null");
WebClient webClient = WebClient.builder()
.defaultHeaders((h) -> h.setBasicAuth(clientId, clientSecret))
.build();
return new SpringReactiveOpaqueTokenIntrospector(this.introspectionUri, webClient);
}

/**
* Creates a {@code SpringReactiveOpaqueTokenIntrospector} with the provided
* parameters
* @param webClient The client for performing the introspection request
* @return the {@link SpringReactiveOpaqueTokenIntrospector}
* @since 6.5
*/
public SpringReactiveOpaqueTokenIntrospector withRestOperations(WebClient webClient) {
Assert.notNull(webClient, "webClient cannot be null");
return new SpringReactiveOpaqueTokenIntrospector(this.introspectionUri, webClient);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector.SpringOpaqueTokenIntrospectorBuilder;
import org.springframework.web.client.RestOperations;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -339,6 +340,48 @@ 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%25%261", "secret%40%242", response));
String introspectUri = server.url("/introspect").toString();
OpaqueTokenIntrospector introspectionClient = SpringOpaqueTokenIntrospectorBuilder
.withIntrospectionUri(introspectUri)
.introspectionEncodeClientCredentials("client%&1", "secret@$2");
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
Expand Up @@ -42,6 +42,7 @@
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector.SpringReactiveOpaqueTokenIntrospectorBuilder;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;

Expand Down Expand Up @@ -261,6 +262,50 @@ 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%25%261", "secret%40%242", response));
String introspectUri = server.url("/introspect").toString();
ReactiveOpaqueTokenIntrospector introspectionClient = SpringReactiveOpaqueTokenIntrospectorBuilder
.withIntrospectionUri(introspectUri)
.introspectionEncodeClientCredentials("client%&1", "secret@$2");
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

0 comments on commit c1f2f7d

Please sign in to comment.