فهرست منبع

Add Introspection Authentication Converter
Closes #14198

Signed-off-by: ahmd-nabil <ahm3dnabil99@gmail.com>

ahmd-nabil 1 سال پیش
والد
کامیت
8279b22940

+ 58 - 15
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.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.
@@ -39,6 +39,7 @@ import org.springframework.http.client.support.BasicAuthenticationInterceptor;
 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.util.LinkedMultiValueMap;
@@ -68,6 +69,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 
 	private Converter<String, RequestEntity<?>> requestEntityConverter;
 
+	private Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter;
+
 	/**
 	 * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
 	 * @param introspectionUri The introspection endpoint uri
@@ -82,11 +85,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		RestTemplate restTemplate = new RestTemplate();
 		restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
 		this.restOperations = restTemplate;
+		this.authenticationConverter = this.defaultAuthenticationConverter();
 	}
 
 	/**
 	 * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
-	 *
 	 * The given {@link RestOperations} should perform its own client authentication
 	 * against the introspection endpoint.
 	 * @param introspectionUri The introspection endpoint uri
@@ -97,6 +100,7 @@ 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) {
@@ -127,7 +131,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		}
 		ResponseEntity<Map<String, Object>> responseEntity = makeRequest(requestEntity);
 		Map<String, Object> claims = adaptToNimbusResponse(responseEntity);
-		return convertClaimsSet(claims);
+		convertClaimsSet(claims);
+		return this.authenticationConverter.convert(() -> claims);
 	}
 
 	/**
@@ -178,7 +183,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		return claims;
 	}
 
-	private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
+	private Map<String, Object> convertClaimsSet(Map<String, Object> claims) {
 		claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
 			if (v instanceof String) {
 				return Collections.singletonList(v);
@@ -211,18 +216,56 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
 		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 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 scopes;
-			}
-			return v;
-		});
-		return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
+				return v;
+			});
+
+			return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
+		};
+	}
+
+	/**
+	 * <p>
+	 * Sets the {@link Converter Converter&lt;OAuth2TokenIntrospectionClaimAccessor,
+	 * OAuth2AuthenticatedPrincipal&gt;} to use. Defaults to
+	 * {@link SpringOpaqueTokenIntrospector#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, OAuth2AuthenticatedPrincipal> authenticationConverter) {
+		Assert.notNull(authenticationConverter, "converter cannot be null");
+		this.authenticationConverter = authenticationConverter;
 	}
 
 }

+ 31 - 1
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospectorTests.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.
@@ -40,6 +40,7 @@ import org.springframework.http.MediaType;
 import org.springframework.http.RequestEntity;
 import org.springframework.http.ResponseEntity;
 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.client.RestOperations;
 
@@ -293,6 +294,35 @@ public class SpringOpaqueTokenIntrospectorTests {
 		verify(requestEntityConverter).convert(tokenToIntrospect);
 	}
 
+	@Test
+	public void setAuthenticationConverterWhenConverterIsNullThenExceptionIsThrown() {
+		RestOperations restOperations = mock(RestOperations.class);
+		SpringOpaqueTokenIntrospector introspectionClient = new SpringOpaqueTokenIntrospector(INTROSPECTION_URL,
+				restOperations);
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> introspectionClient.setAuthenticationConverter(null));
+	}
+
+	@Test
+	public void setAuthenticationConverterWhenNonNullConverterGivenThenConverterUsed() {
+		RestOperations restOperations = mock(RestOperations.class);
+		Converter<String, RequestEntity<?>> requestEntityConverter = mock(Converter.class);
+		RequestEntity requestEntity = mock(RequestEntity.class);
+		Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter = mock(
+				Converter.class);
+		OAuth2AuthenticatedPrincipal oAuth2AuthenticatedPrincipal = mock(OAuth2AuthenticatedPrincipal.class);
+		String tokenToIntrospect = "some token";
+		given(requestEntityConverter.convert(tokenToIntrospect)).willReturn(requestEntity);
+		given(restOperations.exchange(requestEntity, STRING_OBJECT_MAP)).willReturn(response(ACTIVE_RESPONSE));
+		given(authenticationConverter.convert(any())).willReturn(oAuth2AuthenticatedPrincipal);
+		SpringOpaqueTokenIntrospector introspectionClient = new SpringOpaqueTokenIntrospector(INTROSPECTION_URL,
+				restOperations);
+		introspectionClient.setRequestEntityConverter(requestEntityConverter);
+		introspectionClient.setAuthenticationConverter(authenticationConverter);
+		introspectionClient.introspect(tokenToIntrospect);
+		verify(authenticationConverter).convert(any());
+	}
+
 	private static ResponseEntity<Map<String, Object>> response(String content) {
 		HttpHeaders headers = new HttpHeaders();
 		headers.setContentType(MediaType.APPLICATION_JSON);