mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-08 03:32:39 +00:00
Encode Introspection clientId and clientSecret
Closes gh-15988 Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
This commit is contained in:
parent
7c4448c588
commit
aced3bcf16
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.introspection;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -77,9 +79,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||||||
/**
|
/**
|
||||||
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
|
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
|
||||||
* @param introspectionUri The introspection endpoint uri
|
* @param introspectionUri The introspection endpoint uri
|
||||||
* @param clientId The client id authorized to introspect
|
* @param clientId The URL-encoded client id authorized to introspect
|
||||||
* @param clientSecret The client's secret
|
* @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) {
|
public SpringOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
|
||||||
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
|
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
|
||||||
Assert.notNull(clientId, "clientId cannot be null");
|
Assert.notNull(clientId, "clientId cannot be null");
|
||||||
@ -269,6 +273,18 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||||||
return authorities;
|
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
|
// gh-7563
|
||||||
private static final class ArrayListFromString extends ArrayList<String> {
|
private static final class ArrayListFromString extends ArrayList<String> {
|
||||||
|
|
||||||
@ -295,4 +311,61 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.introspection;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -72,9 +74,11 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
|||||||
* Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
|
* Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
|
||||||
* parameters
|
* parameters
|
||||||
* @param introspectionUri The introspection endpoint uri
|
* @param introspectionUri The introspection endpoint uri
|
||||||
* @param clientId The client id authorized to introspect
|
* @param clientId The URL-encoded client id authorized to introspect
|
||||||
* @param clientSecret The client secret for the authorized client
|
* @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) {
|
public SpringReactiveOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
|
||||||
Assert.hasText(introspectionUri, "introspectionUri cannot be empty");
|
Assert.hasText(introspectionUri, "introspectionUri cannot be empty");
|
||||||
Assert.hasText(clientId, "clientId cannot be empty");
|
Assert.hasText(clientId, "clientId cannot be empty");
|
||||||
@ -223,6 +227,18 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
|||||||
return authorities;
|
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
|
// gh-7563
|
||||||
private static final class ArrayListFromString extends ArrayList<String> {
|
private static final class ArrayListFromString extends ArrayList<String> {
|
||||||
|
|
||||||
@ -249,4 +265,62 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -339,6 +339,50 @@ public class SpringOpaqueTokenIntrospectorTests {
|
|||||||
verify(authenticationConverter).convert(any());
|
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) {
|
private static ResponseEntity<Map<String, Object>> response(String content) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -261,6 +261,52 @@ public class SpringReactiveOpaqueTokenIntrospectorTests {
|
|||||||
.isThrownBy(() -> new SpringReactiveOpaqueTokenIntrospector(INTROSPECTION_URL, null));
|
.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) {
|
private WebClient mockResponse(String response) {
|
||||||
return mockResponse(toMap(response));
|
return mockResponse(toMap(response));
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user