소스 검색

Polish Introspection Authentication Converter

- Added Reactive Support
- Separated SCOPE claim and authorities work
- Adjusted for style

Issue gh-14198
Josh Cummings 1 년 전
부모
커밋
10d88cdf28

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

@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.logging.Log;
@@ -69,7 +70,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 
 	private Converter<String, RequestEntity<?>> requestEntityConverter;
 
-	private Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter;
+	private Converter<OAuth2TokenIntrospectionClaimAccessor, ? extends OAuth2AuthenticatedPrincipal> authenticationConverter = this::defaultAuthenticationConverter;
 
 	/**
 	 * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
@@ -85,7 +86,6 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		RestTemplate restTemplate = new RestTemplate();
 		restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
 		this.restOperations = restTemplate;
-		this.authenticationConverter = this.defaultAuthenticationConverter();
 	}
 
 	/**
@@ -100,7 +100,6 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		Assert.notNull(restOperations, "restOperations cannot be null");
 		this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri));
 		this.restOperations = restOperations;
-		this.authenticationConverter = this.defaultAuthenticationConverter();
 	}
 
 	private Converter<String, RequestEntity<?>> defaultRequestEntityConverter(URI introspectionUri) {
@@ -131,8 +130,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		}
 		ResponseEntity<Map<String, Object>> responseEntity = makeRequest(requestEntity);
 		Map<String, Object> claims = adaptToNimbusResponse(responseEntity);
-		convertClaimsSet(claims);
-		return this.authenticationConverter.convert(() -> claims);
+		OAuth2TokenIntrospectionClaimAccessor accessor = convertClaimsSet(claims);
+		return this.authenticationConverter.convert(accessor);
 	}
 
 	/**
@@ -183,7 +182,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		return claims;
 	}
 
-	private Map<String, Object> convertClaimsSet(Map<String, Object> claims) {
+	private OAuth2TokenIntrospectionClaimAccessor convertClaimsSet(Map<String, Object> claims) {
 		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
 			if (v instanceof String) {
 				return Collections.singletonList(v);
@@ -216,44 +215,16 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
 		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
 				(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
-		return claims;
-	}
-
-	/**
-	 * If {@link SpringOpaqueTokenIntrospector#authenticationConverter} is not explicitly
-	 * set, this default converter will be used. transforms an
-	 * {@link OAuth2TokenIntrospectionClaimAccessor} into an
-	 * {@link OAuth2AuthenticatedPrincipal} by extracting claims, mapping scopes to
-	 * authorities, and creating a principal.
-	 * @return {@link Converter Converter&lt;OAuth2TokenIntrospectionClaimAccessor,
-	 * OAuth2AuthenticatedPrincipal&gt;}
-	 * @since 6.3
-	 */
-	private Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> defaultAuthenticationConverter() {
-		return (accessor) -> {
-			Map<String, Object> claims = accessor.getClaims();
-			Collection<GrantedAuthority> authorities = new ArrayList<>();
-
-			claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
-				if (v instanceof String) {
-					Collection<String> scopes = Arrays.asList(((String) v).split(" "));
-					for (String scope : scopes) {
-						authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope));
-					}
-					return scopes;
-				}
-				return v;
-			});
-
-			return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
-		};
+		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE,
+				(k, v) -> (v instanceof String s) ? new ArrayListFromString(s.split(" ")) : v);
+		return () -> claims;
 	}
 
 	/**
 	 * <p>
 	 * Sets the {@link Converter Converter&lt;OAuth2TokenIntrospectionClaimAccessor,
 	 * OAuth2AuthenticatedPrincipal&gt;} to use. Defaults to
-	 * {@link SpringOpaqueTokenIntrospector#defaultAuthenticationConverter()}.
+	 * {@link SpringOpaqueTokenIntrospector#defaultAuthenticationConverter}.
 	 * </p>
 	 * <p>
 	 * Use if you need a custom mapping of OAuth 2.0 token claims to the authenticated
@@ -263,9 +234,45 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 	 * @since 6.3
 	 */
 	public void setAuthenticationConverter(
-			Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter) {
+			Converter<OAuth2TokenIntrospectionClaimAccessor, ? extends OAuth2AuthenticatedPrincipal> authenticationConverter) {
 		Assert.notNull(authenticationConverter, "converter cannot be null");
 		this.authenticationConverter = authenticationConverter;
 	}
 
+	/**
+	 * If {@link SpringOpaqueTokenIntrospector#authenticationConverter} is not explicitly
+	 * set, this default converter will be used. transforms an
+	 * {@link OAuth2TokenIntrospectionClaimAccessor} into an
+	 * {@link OAuth2AuthenticatedPrincipal} by extracting claims, mapping scopes to
+	 * authorities, and creating a principal.
+	 * @return {@link Converter Converter&lt;OAuth2TokenIntrospectionClaimAccessor,
+	 * OAuth2AuthenticatedPrincipal&gt;}
+	 * @since 6.3
+	 */
+	private OAuth2IntrospectionAuthenticatedPrincipal defaultAuthenticationConverter(
+			OAuth2TokenIntrospectionClaimAccessor accessor) {
+		Collection<GrantedAuthority> authorities = authorities(accessor.getScopes());
+		return new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities);
+	}
+
+	private Collection<GrantedAuthority> authorities(List<String> scopes) {
+		if (!(scopes instanceof ArrayListFromString)) {
+			return Collections.emptyList();
+		}
+		Collection<GrantedAuthority> authorities = new ArrayList<>();
+		for (String scope : scopes) {
+			authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope));
+		}
+		return authorities;
+	}
+
+	// gh-7563
+	private static final class ArrayListFromString extends ArrayList<String> {
+
+		ArrayListFromString(String... elements) {
+			super(Arrays.asList(elements));
+		}
+
+	}
+
 }

+ 57 - 14
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -22,11 +22,13 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 import reactor.core.publisher.Mono;
 
 import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.io.buffer.DataBuffer;
 import org.springframework.core.io.buffer.DataBufferUtils;
 import org.springframework.http.HttpHeaders;
@@ -35,6 +37,7 @@ import org.springframework.http.MediaType;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
 import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
 import org.springframework.util.Assert;
 import org.springframework.web.reactive.function.BodyInserters;
@@ -61,6 +64,8 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
 
 	private final WebClient webClient;
 
+	private Converter<OAuth2TokenIntrospectionClaimAccessor, Mono<? extends OAuth2AuthenticatedPrincipal>> authenticationConverter = this::defaultAuthenticationConverter;
+
 	/**
 	 * Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
 	 * parameters
@@ -96,6 +101,8 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
 				.flatMap(this::makeRequest)
 				.flatMap(this::adaptToNimbusResponse)
 				.map(this::convertClaimsSet)
+				.flatMap(this.authenticationConverter::convert)
+				.cast(OAuth2AuthenticatedPrincipal.class)
 				.onErrorMap((e) -> !(e instanceof OAuth2IntrospectionException), this::onError);
 		// @formatter:on
 	}
@@ -135,7 +142,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
 			.switchIfEmpty(Mono.error(() -> new BadOpaqueTokenException("Provided token isn't active")));
 	}
 
-	private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
+	private OAuth2TokenIntrospectionClaimAccessor convertClaimsSet(Map<String, Object> claims) {
 		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
 			if (v instanceof String) {
 				return Collections.singletonList(v);
@@ -168,22 +175,58 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
 		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
 		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
 				(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
-		Collection<GrantedAuthority> authorities = new ArrayList<>();
-		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
-			if (v instanceof String) {
-				Collection<String> scopes = Arrays.asList(((String) v).split(" "));
-				for (String scope : scopes) {
-					authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope));
-				}
-				return scopes;
-			}
-			return v;
-		});
-		return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
+		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE,
+				(k, v) -> (v instanceof String s) ? new ArrayListFromString(s.split(" ")) : v);
+		return () -> claims;
 	}
 
 	private OAuth2IntrospectionException onError(Throwable ex) {
 		return new OAuth2IntrospectionException(ex.getMessage(), ex);
 	}
 
+	/**
+	 * <p>
+	 * Sets the {@link Converter Converter&lt;OAuth2TokenIntrospectionClaimAccessor,
+	 * OAuth2AuthenticatedPrincipal&gt;} to use. Defaults to
+	 * {@link SpringReactiveOpaqueTokenIntrospector#defaultAuthenticationConverter}.
+	 * </p>
+	 * <p>
+	 * Use if you need a custom mapping of OAuth 2.0 token claims to the authenticated
+	 * principal.
+	 * </p>
+	 * @param authenticationConverter the converter
+	 * @since 6.3
+	 */
+	public void setAuthenticationConverter(
+			Converter<OAuth2TokenIntrospectionClaimAccessor, Mono<? extends OAuth2AuthenticatedPrincipal>> authenticationConverter) {
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+		this.authenticationConverter = authenticationConverter;
+	}
+
+	private Mono<OAuth2IntrospectionAuthenticatedPrincipal> defaultAuthenticationConverter(
+			OAuth2TokenIntrospectionClaimAccessor accessor) {
+		Collection<GrantedAuthority> authorities = authorities(accessor.getScopes());
+		return Mono.just(new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities));
+	}
+
+	private Collection<GrantedAuthority> authorities(List<String> scopes) {
+		if (!(scopes instanceof ArrayListFromString)) {
+			return Collections.emptyList();
+		}
+		Collection<GrantedAuthority> authorities = new ArrayList<>();
+		for (String scope : scopes) {
+			authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope));
+		}
+		return authorities;
+	}
+
+	// gh-7563
+	private static final class ArrayListFromString extends ArrayList<String> {
+
+		ArrayListFromString(String... elements) {
+			super(Arrays.asList(elements));
+		}
+
+	}
+
 }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -33,10 +33,12 @@ import org.junit.jupiter.api.Test;
 import reactor.core.publisher.Mono;
 
 import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
 import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
 import org.springframework.web.reactive.function.client.ClientResponse;
 import org.springframework.web.reactive.function.client.WebClient;
@@ -44,9 +46,11 @@ import org.springframework.web.reactive.function.client.WebClient;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
 /**
  * Tests for {@link SpringReactiveOpaqueTokenIntrospector}
@@ -193,6 +197,30 @@ public class SpringReactiveOpaqueTokenIntrospectorTests {
 		// @formatter:on
 	}
 
+	@Test
+	public void setAuthenticationConverterWhenConverterIsNullThenExceptionIsThrown() {
+		WebClient web = mock(WebClient.class);
+		SpringReactiveOpaqueTokenIntrospector introspectionClient = new SpringReactiveOpaqueTokenIntrospector(
+				INTROSPECTION_URL, web);
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> introspectionClient.setAuthenticationConverter(null));
+	}
+
+	@Test
+	public void setAuthenticationConverterWhenNonNullConverterGivenThenConverterUsed() {
+		WebClient web = mockResponse(ACTIVE_RESPONSE);
+		Converter<OAuth2TokenIntrospectionClaimAccessor, Mono<? extends OAuth2AuthenticatedPrincipal>> authenticationConverter = mock(
+				Converter.class);
+		OAuth2AuthenticatedPrincipal oAuth2AuthenticatedPrincipal = mock(OAuth2AuthenticatedPrincipal.class);
+		String tokenToIntrospect = "some token";
+		given(authenticationConverter.convert(any())).willReturn((Mono) Mono.just(oAuth2AuthenticatedPrincipal));
+		SpringReactiveOpaqueTokenIntrospector introspectionClient = new SpringReactiveOpaqueTokenIntrospector(
+				INTROSPECTION_URL, web);
+		introspectionClient.setAuthenticationConverter(authenticationConverter);
+		introspectionClient.introspect(tokenToIntrospect).block();
+		verify(authenticationConverter).convert(any());
+	}
+
 	@Test
 	public void constructorWhenIntrospectionUriIsEmptyThenIllegalArgumentException() {
 		assertThatIllegalArgumentException()