浏览代码

Merge branch '5.8.x'

Steve Riesenberg 2 年之前
父节点
当前提交
2431dd1103
共有 19 个文件被更改,包括 552 次插入45 次删除
  1. 30 3
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
  2. 8 0
      config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java
  3. 25 2
      config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
  4. 4 1
      config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/OpaqueTokenDsl.kt
  5. 4 1
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt
  6. 3 0
      config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc
  7. 7 0
      config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd
  8. 43 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java
  9. 55 0
      config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java
  10. 60 0
      config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java
  11. 37 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml
  12. 4 0
      docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc
  13. 18 3
      docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc
  14. 53 9
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java
  15. 52 24
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java
  16. 39 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java
  17. 41 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java
  18. 34 1
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java
  19. 35 1
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java

+ 30 - 3
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java

@@ -44,6 +44,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
 import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
@@ -105,8 +106,8 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
  * </ul>
  *
  * <p>
- * When using {@link #opaqueToken(Customizer)}, supply an introspection endpoint and its
- * authentication configuration
+ * When using {@link #opaqueToken(Customizer)}, supply an introspection endpoint with its
+ * client credentials and an OpaqueTokenAuthenticationConverter
  * </p>
  *
  * <h2>Security Filters</h2>
@@ -136,6 +137,7 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
  *
  * @author Josh Cummings
  * @author Evgeniy Cheban
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
  * @since 5.1
  * @see BearerTokenAuthenticationFilter
  * @see JwtAuthenticationProvider
@@ -448,6 +450,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 
 		private Supplier<OpaqueTokenIntrospector> introspector;
 
+		private OpaqueTokenAuthenticationConverter authenticationConverter;
+
 		OpaqueTokenConfigurer(ApplicationContext context) {
 			this.context = context;
 		}
@@ -482,6 +486,13 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 			return this;
 		}
 
+		public OpaqueTokenConfigurer authenticationConverter(
+				OpaqueTokenAuthenticationConverter authenticationConverter) {
+			Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+			this.authenticationConverter = authenticationConverter;
+			return this;
+		}
+
 		OpaqueTokenIntrospector getIntrospector() {
 			if (this.introspector != null) {
 				return this.introspector.get();
@@ -489,12 +500,28 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 			return this.context.getBean(OpaqueTokenIntrospector.class);
 		}
 
+		OpaqueTokenAuthenticationConverter getAuthenticationConverter() {
+			if (this.authenticationConverter != null) {
+				return this.authenticationConverter;
+			}
+			if (this.context.getBeanNamesForType(OpaqueTokenAuthenticationConverter.class).length > 0) {
+				return this.context.getBean(OpaqueTokenAuthenticationConverter.class);
+			}
+			return null;
+		}
+
 		AuthenticationProvider getAuthenticationProvider() {
 			if (this.authenticationManager != null) {
 				return null;
 			}
 			OpaqueTokenIntrospector introspector = getIntrospector();
-			return new OpaqueTokenAuthenticationProvider(introspector);
+			OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider(
+					introspector);
+			OpaqueTokenAuthenticationConverter authenticationConverter = getAuthenticationConverter();
+			if (authenticationConverter != null) {
+				opaqueTokenAuthenticationProvider.setAuthenticationConverter(authenticationConverter);
+			}
+			return opaqueTokenAuthenticationProvider;
 		}
 
 		AuthenticationManager getAuthenticationManager(H http) {

+ 8 - 0
config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java

@@ -244,6 +244,10 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
 
 		static final String CLIENT_SECRET = "client-secret";
 
+		static final String AUTHENTICATION_CONVERTER_REF = "authentication-converter-ref";
+
+		static final String AUTHENTICATION_CONVERTER = "authenticationConverter";
+
 		OpaqueTokenBeanDefinitionParser() {
 		}
 
@@ -251,9 +255,13 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
 		public BeanDefinition parse(Element element, ParserContext pc) {
 			validateConfiguration(element, pc);
 			BeanMetadataElement introspector = getIntrospector(element);
+			String authenticationConverterRef = element.getAttribute(AUTHENTICATION_CONVERTER_REF);
 			BeanDefinitionBuilder opaqueTokenProviderBuilder = BeanDefinitionBuilder
 					.rootBeanDefinition(OpaqueTokenAuthenticationProvider.class);
 			opaqueTokenProviderBuilder.addConstructorArgValue(introspector);
+			if (StringUtils.hasText(authenticationConverterRef)) {
+				opaqueTokenProviderBuilder.addPropertyReference(AUTHENTICATION_CONVERTER, authenticationConverterRef);
+			}
 			return opaqueTokenProviderBuilder.getBeanDefinition();
 		}
 

+ 25 - 2
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -95,6 +95,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtRea
 import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager;
 import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector;
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;
 import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
@@ -4285,6 +4286,8 @@ public class ServerHttpSecurity {
 
 			private Supplier<ReactiveOpaqueTokenIntrospector> introspector;
 
+			private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter;
+
 			private OpaqueTokenSpec() {
 			}
 
@@ -4323,6 +4326,13 @@ public class ServerHttpSecurity {
 				return this;
 			}
 
+			public OpaqueTokenSpec authenticationConverter(
+					ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) {
+				Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+				this.authenticationConverter = authenticationConverter;
+				return this;
+			}
+
 			/**
 			 * Allows method chaining to continue configuring the
 			 * {@link ServerHttpSecurity}
@@ -4333,7 +4343,13 @@ public class ServerHttpSecurity {
 			}
 
 			protected ReactiveAuthenticationManager getAuthenticationManager() {
-				return new OpaqueTokenReactiveAuthenticationManager(getIntrospector());
+				OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager(
+						getIntrospector());
+				ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = getAuthenticationConverter();
+				if (authenticationConverter != null) {
+					authenticationManager.setAuthenticationConverter(authenticationConverter);
+				}
+				return authenticationManager;
 			}
 
 			protected ReactiveOpaqueTokenIntrospector getIntrospector() {
@@ -4343,6 +4359,13 @@ public class ServerHttpSecurity {
 				return getBean(ReactiveOpaqueTokenIntrospector.class);
 			}
 
+			protected ReactiveOpaqueTokenAuthenticationConverter getAuthenticationConverter() {
+				if (this.authenticationConverter != null) {
+					return this.authenticationConverter;
+				}
+				return getBeanOrNull(ReactiveOpaqueTokenAuthenticationConverter.class);
+			}
+
 			protected void configure(ServerHttpSecurity http) {
 				ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
 				AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);

+ 4 - 1
config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/OpaqueTokenDsl.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -20,6 +20,7 @@ import org.springframework.security.authentication.AuthenticationManager
 import org.springframework.security.config.annotation.web.builders.HttpSecurity
 import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
 import org.springframework.security.core.Authentication
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter
 import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector
 
 /**
@@ -54,6 +55,7 @@ class OpaqueTokenDsl {
             clientCredentials = null
         }
 
+    var authenticationConverter: OpaqueTokenAuthenticationConverter? = null
 
     /**
      * Configures the credentials for Introspection endpoint.
@@ -70,6 +72,7 @@ class OpaqueTokenDsl {
         return { opaqueToken ->
             introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) }
             introspector?.also { opaqueToken.introspector(introspector) }
+            authenticationConverter?.also { opaqueToken.authenticationConverter(authenticationConverter) }
             clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) }
             authenticationManager?.also { opaqueToken.authenticationManager(authenticationManager) }
         }

+ 4 - 1
config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -16,6 +16,7 @@
 
 package org.springframework.security.config.web.server
 
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter
 import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector
 
 /**
@@ -45,6 +46,7 @@ class ServerOpaqueTokenDsl {
             _introspectionUri = null
             clientCredentials = null
         }
+    var authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? = null
 
     /**
      * Configures the credentials for Introspection endpoint.
@@ -62,6 +64,7 @@ class ServerOpaqueTokenDsl {
             introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) }
             clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) }
             introspector?.also { opaqueToken.introspector(introspector) }
+            authenticationConverter?.also { opaqueToken.authenticationConverter(authenticationConverter) }
         }
     }
 }

+ 3 - 0
config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc

@@ -667,6 +667,9 @@ opaque-token.attlist &=
 opaque-token.attlist &=
     ## Reference to an OpaqueTokenIntrospector
     attribute introspector-ref {xsd:token}?
+opaque-token.attlist &=
+    ## Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful introspection result into an Authentication.
+    attribute authentication-converter-ref {xsd:token}?
 
 openid-login =
 	## Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are <a href="https://openid.net/specs/openid-connect-migration-1_0.html">encouraged to migrate</a> to <a href="https://openid.net/connect/">OpenID Connect</a>, which is supported by <code>spring-security-oauth2</code>.

+ 7 - 0
config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd

@@ -2060,6 +2060,13 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="authentication-converter-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful
+                introspection result into an Authentication.
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
   </xs:attributeGroup>
   
   <xs:element name="attribute-exchange">

+ 43 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

@@ -80,6 +80,7 @@ import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
@@ -98,6 +99,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
 import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
 import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
 import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -116,6 +118,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
 import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
@@ -1353,6 +1356,22 @@ public class OAuth2ResourceServerConfigurerTests {
 				.isThrownBy(jwtConfigurer::getJwtAuthenticationConverter);
 	}
 
+	@Test
+	public void getWhenCustomAuthenticationConverterThenConverts() throws Exception {
+		this.spring.register(RestOperationsConfig.class, OpaqueTokenAuthenticationConverterConfig.class,
+				BasicController.class).autowire();
+		OpaqueTokenAuthenticationConverter authenticationConverter = this.spring.getContext()
+				.getBean(OpaqueTokenAuthenticationConverter.class);
+		given(authenticationConverter.convert(anyString(), any(OAuth2AuthenticatedPrincipal.class)))
+				.willReturn(new TestingAuthenticationToken("jdoe", null, Collections.emptyList()));
+		mockRestOperations(json("Active"));
+		// @formatter:off
+		this.mvc.perform(get("/authenticated").with(bearerToken("token")))
+				.andExpect(status().isOk())
+				.andExpect(content().string("jdoe"));
+		// @formatter:on
+	}
+
 	private static <T> void registerMockBean(GenericApplicationContext context, String name, Class<T> clazz) {
 		context.registerBean(name, clazz, () -> mock(clazz));
 	}
@@ -2444,6 +2463,30 @@ public class OAuth2ResourceServerConfigurerTests {
 
 	}
 
+	@EnableWebSecurity
+	static class OpaqueTokenAuthenticationConverterConfig extends WebSecurityConfigurerAdapter {
+
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.authorizeRequests()
+					.antMatchers("/requires-read-scope").hasAuthority("SCOPE_message:read")
+					.anyRequest().authenticated()
+					.and()
+					.oauth2ResourceServer()
+					.opaqueToken()
+					.authenticationConverter(authenticationConverter());
+			// @formatter:on
+		}
+
+		@Bean
+		OpaqueTokenAuthenticationConverter authenticationConverter() {
+			return mock(OpaqueTokenAuthenticationConverter.class);
+		}
+
+	}
+
 	@Configuration
 	static class JwtDecoderConfig {
 

+ 55 - 0
config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java

@@ -23,6 +23,7 @@ import java.security.interfaces.RSAPublicKey;
 import java.time.Clock;
 import java.time.Instant;
 import java.time.ZoneId;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -66,12 +67,16 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.RequestEntity;
 import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.JwtBeanDefinitionParser;
 import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.OpaqueTokenBeanDefinitionParser;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
@@ -84,6 +89,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.oauth2.jwt.TestJwts;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
 import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
@@ -643,6 +649,20 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
 		// @formatter:on
 	}
 
+	@Test
+	public void configureWhenIntrospectingWithAuthenticationConverterThenUses() throws Exception {
+		this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueTokenAndAuthenticationConverter"))
+				.autowire();
+		mockRestOperations(json("Active"));
+		// @formatter:off
+		this.mvc.perform(get("/authenticated").header("Authorization", "Bearer token"))
+				.andExpect(status().isNotFound());
+
+		this.mvc.perform(get("/authenticated").header("Authorization", "Bearer invalidToken"))
+				.andExpect(status().isUnauthorized());
+		// @formatter:on
+	}
+
 	@Test
 	public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
 		this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
@@ -1077,4 +1097,39 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
 
 	}
 
+	public static class TestAuthentication extends AbstractAuthenticationToken {
+
+		private final String introspectedToken;
+
+		public TestAuthentication(String introspectedToken, Collection<? extends GrantedAuthority> authorities) {
+			super(authorities);
+			this.introspectedToken = introspectedToken;
+		}
+
+		@Override
+		public Object getCredentials() {
+			return this.introspectedToken;
+		}
+
+		@Override
+		public Object getPrincipal() {
+			return this.introspectedToken;
+		}
+
+		@Override
+		public boolean isAuthenticated() {
+			return "token".equals(this.introspectedToken);
+		}
+
+	}
+
+	public static class TestOpaqueTokenAuthenticationConverter implements OpaqueTokenAuthenticationConverter {
+
+		@Override
+		public Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) {
+			return new TestAuthentication(introspectedToken, Collections.emptyList());
+		}
+
+	}
+
 }

+ 60 - 0
config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java

@@ -24,6 +24,7 @@ import java.security.interfaces.RSAPublicKey;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.RSAPublicKeySpec;
 import java.util.Base64;
+import java.util.Collections;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -52,11 +53,13 @@ import org.springframework.http.MediaType;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
+import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -66,6 +69,7 @@ import org.springframework.security.oauth2.server.resource.BearerTokenAuthentica
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
 import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
@@ -566,6 +570,25 @@ public class OAuth2ResourceServerSpecTests {
 				.withMessageContaining("authenticationManagerResolver");
 	}
 
+	@Test
+	public void getWhenCustomAuthenticationConverterThenConverts() {
+		this.spring.register(ReactiveOpaqueTokenAuthenticationConverterConfig.class, RootController.class).autowire();
+		this.spring.getContext().getBean(MockWebServer.class)
+				.setDispatcher(requiresAuth(this.clientId, this.clientSecret, this.active));
+		ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = this.spring.getContext()
+				.getBean(ReactiveOpaqueTokenAuthenticationConverter.class);
+		given(authenticationConverter.convert(anyString(), any(OAuth2AuthenticatedPrincipal.class)))
+				.willReturn(Mono.just(new TestingAuthenticationToken("jdoe", null, Collections.emptyList())));
+		// @formatter:off
+		this.client.get()
+				.headers((headers) -> headers
+						.setBearerAuth(this.messageReadToken)
+				)
+				.exchange()
+				.expectStatus().isOk();
+		// @formatter:on
+	}
+
 	private static Dispatcher requiresAuth(String username, String password, String response) {
 		return new Dispatcher() {
 			@Override
@@ -1052,6 +1075,43 @@ public class OAuth2ResourceServerSpecTests {
 
 	}
 
+	@EnableWebFlux
+	@EnableWebFluxSecurity
+	static class ReactiveOpaqueTokenAuthenticationConverterConfig {
+
+		private MockWebServer mockWebServer = new MockWebServer();
+
+		@Bean
+		SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+			String introspectionUri = mockWebServer().url("/introspect").toString();
+			// @formatter:off
+			http
+				.oauth2ResourceServer()
+					.opaqueToken()
+						.introspectionUri(introspectionUri)
+						.introspectionClientCredentials("client", "secret")
+						.authenticationConverter(authenticationConverter());
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		ReactiveOpaqueTokenAuthenticationConverter authenticationConverter() {
+			return mock(ReactiveOpaqueTokenAuthenticationConverter.class);
+		}
+
+		@Bean
+		MockWebServer mockWebServer() {
+			return this.mockWebServer;
+		}
+
+		@PreDestroy
+		void shutdown() throws IOException {
+			this.mockWebServer.shutdown();
+		}
+
+	}
+
 	@RestController
 	static class RootController {
 

+ 37 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2020 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.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xmlns="http://www.springframework.org/schema/security"
+		 xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
+		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<b:bean name="authentication-converter"
+			class="org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParserTests$TestOpaqueTokenAuthenticationConverter">
+	</b:bean>
+
+	<http>
+		<intercept-url pattern="/requires-read-scope" access="hasAuthority('SCOPE_message:read')"/>
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-resource-server>
+			<opaque-token introspector-ref="introspector" authentication-converter-ref="authentication-converter"/>
+		</oauth2-resource-server>
+	</http>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 4 - 0
docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

@@ -1324,6 +1324,10 @@ The Client Id to use for client authentication against the provided `introspecti
 * **client-secret**
 The Client Secret to use for client authentication against the provided `introspection-uri`.
 
+[[nsa-opaque-token-authentication-converter-ref]]
+* **authentication-converter-ref**
+Reference to an `OpaqueTokenAuthenticationConverter`. Responsible for converting successful introspection result into an `Authentication` instance.
+
 
 [[nsa-relying-party-registrations]]
 == <relying-party-registrations>

+ 18 - 3
docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc

@@ -297,11 +297,13 @@ fun introspector(): OpaqueTokenIntrospector {
 ----
 ====
 
-If the application doesn't expose a <<oauth2resourceserver-opaque-architecture-introspector,`OpaqueTokenIntrospector`>> bean, then Spring Boot will expose the above default one.
+If the application doesn't expose an <<oauth2resourceserver-opaque-architecture-introspector,`OpaqueTokenIntrospector`>> bean, then Spring Boot will expose the above default one.
 
 And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`.
 
-Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a <<oauth2resourceserver-opaque-architecture-introspector,`OpaqueTokenIntrospector`>> can be specified in XML.
+If the application doesn't expose an `OpaqueTokenAuthenticationConverter` bean, then spring-security will build `BearerTokenAuthentication`.
+
+Or, if you're not using Spring Boot at all, then all of these components - the filter chain, an <<oauth2resourceserver-opaque-architecture-introspector,`OpaqueTokenIntrospector`>> and an `OpaqueTokenAuthenticationConverter` can be specified in XML.
 
 The filter chain is specified like so:
 
@@ -313,7 +315,8 @@ The filter chain is specified like so:
 <http>
     <intercept-uri pattern="/**" access="authenticated"/>
     <oauth2-resource-server>
-        <opaque-token introspector-ref="opaqueTokenIntrospector"/>
+        <opaque-token introspector-ref="opaqueTokenIntrospector"
+                authentication-converter-ref="opaqueTokenAuthenticationConverter"/>
     </oauth2-resource-server>
 </http>
 ----
@@ -335,6 +338,18 @@ And the <<oauth2resourceserver-opaque-architecture-introspector,`OpaqueTokenIntr
 ----
 ====
 
+And the `OpaqueTokenAuthenticationConverter` like so:
+
+.Opaque Token Authentication Converter
+====
+.Xml
+[source,xml,role="primary"]
+----
+<bean id="opaqueTokenAuthenticationConverter"
+        class="com.example.CustomOpaqueTokenAuthenticationConverter"/>
+----
+====
+
 [[oauth2resourceserver-opaque-introspectionuri-dsl]]
 === Using `introspectionUri()`
 

+ 53 - 9
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -35,6 +35,7 @@ import org.springframework.security.oauth2.server.resource.BearerTokenAuthentica
 import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
 import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException;
 import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
 import org.springframework.util.Assert;
 
@@ -57,8 +58,16 @@ import org.springframework.util.Assert;
  * <li>Take the resulting {@link Collection} and prepend the "SCOPE_" keyword to each
  * element, adding as {@link GrantedAuthority}s.
  * </ol>
+ * <p>
+ * An {@link OpaqueTokenIntrospector} is responsible for retrieving token attributes from
+ * an authorization server.
+ * <p>
+ * An {@link OpaqueTokenAuthenticationConverter} is responsible for turning a successful
+ * introspection result into an {@link Authentication} instance (which may include mapping
+ * {@link GrantedAuthority}s from token attributes or retrieving from another source).
  *
  * @author Josh Cummings
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
  * @since 5.2
  * @see AuthenticationProvider
  */
@@ -68,6 +77,8 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 
 	private final OpaqueTokenIntrospector introspector;
 
+	private OpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenAuthenticationProvider::convert;
+
 	/**
 	 * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
 	 * @param introspector The {@link OpaqueTokenIntrospector} to use
@@ -80,7 +91,11 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 	/**
 	 * Introspect and validate the opaque
 	 * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer
-	 * Token</a>.
+	 * Token</a> and then delegates {@link Authentication} instantiation to
+	 * {@link OpaqueTokenAuthenticationConverter}.
+	 * <p>
+	 * If created Authentication is instance of {@link AbstractAuthenticationToken} and
+	 * details are null, then introspection result details are used.
 	 * @param authentication the authentication request object.
 	 * @return A successful authentication
 	 * @throws AuthenticationException if authentication failed for some reason
@@ -92,8 +107,16 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 		}
 		BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
 		OAuth2AuthenticatedPrincipal principal = getOAuth2AuthenticatedPrincipal(bearer);
-		AbstractAuthenticationToken result = convert(principal, bearer.getToken());
-		result.setDetails(bearer.getDetails());
+		Authentication result = this.authenticationConverter.convert(bearer.getToken(), principal);
+		if (result == null) {
+			return null;
+		}
+		if (AbstractAuthenticationToken.class.isAssignableFrom(result.getClass())) {
+			final AbstractAuthenticationToken auth = (AbstractAuthenticationToken) result;
+			if (auth.getDetails() == null) {
+				auth.setDetails(bearer.getDetails());
+			}
+		}
 		this.logger.debug("Authenticated token");
 		return result;
 	}
@@ -116,11 +139,32 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 		return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
 	}
 
-	private AbstractAuthenticationToken convert(OAuth2AuthenticatedPrincipal principal, String token) {
-		Instant iat = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT);
-		Instant exp = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
-		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp);
-		return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities());
+	/**
+	 * Default {@link OpaqueTokenAuthenticationConverter}.
+	 * @param introspectedToken the bearer string that was successfully introspected
+	 * @param authenticatedPrincipal the successful introspection output
+	 * @return a {@link BearerTokenAuthentication}
+	 */
+	static BearerTokenAuthentication convert(String introspectedToken,
+			OAuth2AuthenticatedPrincipal authenticatedPrincipal) {
+		Instant iat = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT);
+		Instant exp = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken,
+				iat, exp);
+		return new BearerTokenAuthentication(authenticatedPrincipal, accessToken,
+				authenticatedPrincipal.getAuthorities());
+	}
+
+	/**
+	 * Provide with a custom bean to turn successful introspection result into an
+	 * {@link Authentication} instance of your choice. By default,
+	 * {@link BearerTokenAuthentication} will be built.
+	 * @param authenticationConverter the converter to use
+	 * @since 5.8
+	 */
+	public void setAuthenticationConverter(OpaqueTokenAuthenticationConverter authenticationConverter) {
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+		this.authenticationConverter = authenticationConverter;
 	}
 
 }

+ 52 - 24
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -16,22 +16,21 @@
 
 package org.springframework.security.oauth2.server.resource.authentication;
 
-import java.time.Instant;
-import java.util.Collection;
-
 import reactor.core.publisher.Mono;
 
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.oauth2.core.OAuth2AccessToken;
-import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
 import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException;
 import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
 import org.springframework.util.Assert;
 
@@ -46,16 +45,16 @@ import org.springframework.util.Assert;
  * verifying an opaque access token, returning its attributes set as part of the
  * {@link Authentication} statement.
  * <p>
- * Scopes are translated into {@link GrantedAuthority}s according to the following
- * algorithm:
- * <ol>
- * <li>If there is a "scope" attribute, then convert to a {@link Collection} of
- * {@link String}s.
- * <li>Take the resulting {@link Collection} and prepend the "SCOPE_" keyword to each
- * element, adding as {@link GrantedAuthority}s.
- * </ol>
+ * A {@link ReactiveOpaqueTokenIntrospector} is responsible for retrieving token
+ * attributes from an authorization server.
+ * <p>
+ * A {@link ReactiveOpaqueTokenAuthenticationConverter} is responsible for turning a
+ * successful introspection result into an {@link Authentication} instance (which may
+ * include mapping {@link GrantedAuthority}s from token attributes or retrieving from
+ * another source).
  *
  * @author Josh Cummings
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
  * @since 5.2
  * @see ReactiveAuthenticationManager
  */
@@ -63,6 +62,8 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 
 	private final ReactiveOpaqueTokenIntrospector introspector;
 
+	private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenReactiveAuthenticationManager::convert;
+
 	/**
 	 * Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
 	 * parameters
@@ -73,6 +74,17 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 		this.introspector = introspector;
 	}
 
+	/**
+	 * Introspect and validate the opaque
+	 * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer
+	 * Token</a> and then delegates {@link Authentication} instantiation to
+	 * {@link ReactiveOpaqueTokenAuthenticationConverter}.
+	 * <p>
+	 * If created Authentication is instance of {@link AbstractAuthenticationToken} and
+	 * details are null, then introspection result details are used.
+	 * @param authentication the authentication request object.
+	 * @return A successful authentication
+	 */
 	@Override
 	public Mono<Authentication> authenticate(Authentication authentication) {
 		// @formatter:off
@@ -80,21 +92,14 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 				.filter(BearerTokenAuthenticationToken.class::isInstance)
 				.cast(BearerTokenAuthenticationToken.class)
 				.map(BearerTokenAuthenticationToken::getToken)
-				.flatMap(this::authenticate)
-				.cast(Authentication.class);
+				.flatMap(this::authenticate);
 		// @formatter:on
 	}
 
-	private Mono<BearerTokenAuthentication> authenticate(String token) {
+	private Mono<Authentication> authenticate(String token) {
 		// @formatter:off
 		return this.introspector.introspect(token)
-				.map((principal) -> {
-					Instant iat = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT);
-					Instant exp = principal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
-					// construct token
-					OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, iat, exp);
-					return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities());
-				})
+				.flatMap((principal) -> this.authenticationConverter.convert(token, principal))
 				.onErrorMap(OAuth2IntrospectionException.class, this::onError);
 		// @formatter:on
 	}
@@ -106,4 +111,27 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 		return new AuthenticationServiceException(ex.getMessage(), ex);
 	}
 
+	/**
+	 * Default {@link ReactiveOpaqueTokenAuthenticationConverter}.
+	 * @param introspectedToken the bearer string that was successfully introspected
+	 * @param authenticatedPrincipal the successful introspection output
+	 * @return an async wrapper of default {@link OpaqueTokenAuthenticationConverter}
+	 * result
+	 */
+	static Mono<Authentication> convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) {
+		return Mono.just(OpaqueTokenAuthenticationProvider.convert(introspectedToken, authenticatedPrincipal));
+	}
+
+	/**
+	 * Provide with a custom bean to turn successful introspection result into an
+	 * {@link Authentication} instance of your choice. By default,
+	 * {@link BearerTokenAuthentication} will be built.
+	 * @param authenticationConverter the converter to use
+	 * @since 5.8
+	 */
+	public void setAuthenticationConverter(ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) {
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+		this.authenticationConverter = authenticationConverter;
+	}
+
 }

+ 39 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2002-2022 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.server.resource.introspection;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+
+/**
+ * Convert a successful introspection result into an authentication result.
+ *
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
+ * @since 5.8
+ */
+@FunctionalInterface
+public interface OpaqueTokenAuthenticationConverter {
+
+	/**
+	 * Converts a successful introspection result into an authentication result.
+	 * @param introspectedToken the bearer token used to perform token introspection
+	 * @param authenticatedPrincipal the result of token introspection
+	 * @return an {@link Authentication} instance
+	 */
+	Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal);
+
+}

+ 41 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2002-2021 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.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.server.resource.introspection;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+
+/**
+ * Convert a successful introspection result into an authentication result.
+ *
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
+ * @since 5.8
+ */
+@FunctionalInterface
+public interface ReactiveOpaqueTokenAuthenticationConverter {
+
+	/**
+	 * Converts a successful introspection result into an authentication result.
+	 * @param introspectedToken the bearer token used to perform token introspection
+	 * @param authenticatedPrincipal the result of token introspection
+	 * @return an {@link Authentication} instance
+	 */
+	Mono<Authentication> convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal);
+
+}

+ 34 - 1
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -25,6 +25,7 @@ import java.util.Map;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
@@ -32,6 +33,7 @@ import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipal
 import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
 import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -40,6 +42,8 @@ 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.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 /**
  * Tests for {@link OpaqueTokenAuthenticationProvider}
@@ -114,4 +118,33 @@ public class OpaqueTokenAuthenticationProviderTests {
 		// @formatter:on
 	}
 
+	@Test
+	public void setAuthenticationConverterWhenNullThenThrowsIllegalArgumentException() {
+		OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class);
+		OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector);
+		// @formatter:off
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> provider.setAuthenticationConverter(null))
+				.withMessage("authenticationConverter cannot be null");
+		// @formatter:on
+	}
+
+	@Test
+	public void authenticateWhenCustomAuthenticationConverterThenUses() {
+		OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class);
+		OAuth2AuthenticatedPrincipal principal = TestOAuth2AuthenticatedPrincipals.active();
+		given(introspector.introspect(any())).willReturn(principal);
+		OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector);
+		OpaqueTokenAuthenticationConverter authenticationConverter = mock(OpaqueTokenAuthenticationConverter.class);
+		given(authenticationConverter.convert(any(), any(OAuth2AuthenticatedPrincipal.class)))
+				.willReturn(new TestingAuthenticationToken(principal, null, Collections.emptyList()));
+		provider.setAuthenticationConverter(authenticationConverter);
+
+		Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token"));
+		assertThat(result).isNotNull();
+		verify(introspector).introspect("token");
+		verify(authenticationConverter).convert("token", principal);
+		verifyNoMoreInteractions(introspector, authenticationConverter);
+	}
+
 }

+ 35 - 1
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
 import reactor.core.publisher.Mono;
 
 import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
@@ -33,6 +34,7 @@ import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipal
 import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
 import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
+import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -41,6 +43,8 @@ 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.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 /**
  * Tests for {@link OpaqueTokenReactiveAuthenticationManager}
@@ -112,4 +116,34 @@ public class OpaqueTokenReactiveAuthenticationManagerTests {
 		// @formatter:on
 	}
 
+	@Test
+	public void setAuthenticationConverterWhenNullThenThrowsIllegalArgumentException() {
+		ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class);
+		OpaqueTokenReactiveAuthenticationManager provider = new OpaqueTokenReactiveAuthenticationManager(introspector);
+		// @formatter:off
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> provider.setAuthenticationConverter(null))
+				.withMessage("authenticationConverter cannot be null");
+		// @formatter:on
+	}
+
+	@Test
+	public void authenticateWhenCustomAuthenticationConverterThenUses() {
+		ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class);
+		OAuth2AuthenticatedPrincipal principal = TestOAuth2AuthenticatedPrincipals.active();
+		given(introspector.introspect(any())).willReturn(Mono.just(principal));
+		OpaqueTokenReactiveAuthenticationManager provider = new OpaqueTokenReactiveAuthenticationManager(introspector);
+		ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = mock(
+				ReactiveOpaqueTokenAuthenticationConverter.class);
+		given(authenticationConverter.convert(any(), any(OAuth2AuthenticatedPrincipal.class)))
+				.willReturn(Mono.just(new TestingAuthenticationToken(principal, null, Collections.emptyList())));
+		provider.setAuthenticationConverter(authenticationConverter);
+
+		Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token")).block();
+		assertThat(result).isNotNull();
+		verify(introspector).introspect("token");
+		verify(authenticationConverter).convert("token", principal);
+		verifyNoMoreInteractions(introspector, authenticationConverter);
+	}
+
 }