瀏覽代碼

Encode Introspection clientId and clientSecret

Closes gh-15988

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
Tran Ngoc Nhan 8 月之前
父節點
當前提交
aced3bcf16

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

@@ -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.
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.introspection;
 
 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;
@@ -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");
@@ -269,6 +273,18 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		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> {
 
@@ -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);
+		}
+
+	}
+
 }

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

@@ -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.
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.introspection;
 
 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;
@@ -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");
@@ -223,6 +227,18 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
 		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> {
 
@@ -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);
+		}
+
+	}
+
 }

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

@@ -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.
@@ -339,6 +339,50 @@ public class SpringOpaqueTokenIntrospectorTests {
 		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);

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

@@ -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.
@@ -261,6 +261,52 @@ public class SpringReactiveOpaqueTokenIntrospectorTests {
 			.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));
 	}