瀏覽代碼

Add authentication converter for introspected tokens

Adds configurable authentication converter for resource-servers with
token introspection (something very similar to what
JwtAuthenticationConverter does for resource-servers with JWT decoder).

The new (Reactive)OpaqueTokenAuthenticationConverter is given
responsibility for converting successful token introspection result
into an Authentication instance (which is currently done by a private
methods of OpaqueTokenAuthenticationProvider and
OpaqueTokenReactiveAuthenticationManager).

The default (Reactive)OpaqueTokenAuthenticationConverter, behave the
same as current private convert(OAuth2AuthenticatedPrincipal principal,
String token) methods: map authorities from scope attribute and build a
BearerTokenAuthentication.

Closes gh-11661
ch4mpy 3 年之前
父節點
當前提交
1efb63387f
共有 16 個文件被更改,包括 385 次插入48 次删除
  1. 31 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. 29 1
      config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
  4. 11 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt
  5. 8 0
      config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.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. 55 0
      config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java
  9. 37 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml
  10. 4 0
      docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc
  11. 4 2
      docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc
  12. 62 17
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java
  13. 57 24
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java
  14. 33 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java
  15. 35 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java
  16. 1 1
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java

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

@@ -21,10 +21,12 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 import java.util.function.Supplier;
 import java.util.function.Supplier;
 
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
 
 
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.MediaType;
 import org.springframework.http.MediaType;
@@ -46,6 +48,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.JwtAuthenticationConverter;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
 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.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.OpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
@@ -107,8 +110,8 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
  * </ul>
  * </ul>
  *
  *
  * <p>
  * <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>
  * </p>
  *
  *
  * <h2>Security Filters</h2>
  * <h2>Security Filters</h2>
@@ -138,6 +141,7 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
  *
  *
  * @author Josh Cummings
  * @author Josh Cummings
  * @author Evgeniy Cheban
  * @author Evgeniy Cheban
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
  * @since 5.1
  * @since 5.1
  * @see BearerTokenAuthenticationFilter
  * @see BearerTokenAuthenticationFilter
  * @see JwtAuthenticationProvider
  * @see JwtAuthenticationProvider
@@ -456,6 +460,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 
 
 		private Supplier<OpaqueTokenIntrospector> introspector;
 		private Supplier<OpaqueTokenIntrospector> introspector;
 
 
+		private Supplier<OpaqueTokenAuthenticationConverter> authenticationConverter;
+
 		OpaqueTokenConfigurer(ApplicationContext context) {
 		OpaqueTokenConfigurer(ApplicationContext context) {
 			this.context = context;
 			this.context = context;
 		}
 		}
@@ -490,6 +496,13 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 			return this;
 			return this;
 		}
 		}
 
 
+		public OpaqueTokenConfigurer authenticationConverter(
+				OpaqueTokenAuthenticationConverter authenticationConverter) {
+			Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+			this.authenticationConverter = () -> authenticationConverter;
+			return this;
+		}
+
 		OpaqueTokenIntrospector getIntrospector() {
 		OpaqueTokenIntrospector getIntrospector() {
 			if (this.introspector != null) {
 			if (this.introspector != null) {
 				return this.introspector.get();
 				return this.introspector.get();
@@ -497,12 +510,27 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 			return this.context.getBean(OpaqueTokenIntrospector.class);
 			return this.context.getBean(OpaqueTokenIntrospector.class);
 		}
 		}
 
 
+		Optional<OpaqueTokenAuthenticationConverter> getAuthenticationConverter() {
+			if (this.authenticationConverter != null) {
+				return Optional.of(this.authenticationConverter.get());
+			}
+			try {
+				return Optional.of(this.context.getBean(OpaqueTokenAuthenticationConverter.class));
+			}
+			catch (NoSuchBeanDefinitionException nsbde) {
+				return Optional.empty();
+			}
+		}
+
 		AuthenticationProvider getAuthenticationProvider() {
 		AuthenticationProvider getAuthenticationProvider() {
 			if (this.authenticationManager != null) {
 			if (this.authenticationManager != null) {
 				return null;
 				return null;
 			}
 			}
 			OpaqueTokenIntrospector introspector = getIntrospector();
 			OpaqueTokenIntrospector introspector = getIntrospector();
-			return new OpaqueTokenAuthenticationProvider(introspector);
+			final OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider(
+					introspector);
+			getAuthenticationConverter().ifPresent(opaqueTokenAuthenticationProvider::setAuthenticationConverter);
+			return opaqueTokenAuthenticationProvider;
 		}
 		}
 
 
 		AuthenticationManager getAuthenticationManager(H http) {
 		AuthenticationManager getAuthenticationManager(H http) {

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

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

+ 29 - 1
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -27,6 +27,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 import java.util.UUID;
 import java.util.UUID;
 import java.util.function.Function;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.function.Supplier;
@@ -35,6 +36,7 @@ import reactor.core.publisher.Mono;
 import reactor.util.context.Context;
 import reactor.util.context.Context;
 
 
 import org.springframework.beans.BeansException;
 import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
 import org.springframework.core.Ordered;
 import org.springframework.core.Ordered;
 import org.springframework.core.ResolvableType;
 import org.springframework.core.ResolvableType;
@@ -95,6 +97,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.OpaqueTokenReactiveAuthenticationManager;
 import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
 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.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.introspection.ReactiveOpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;
 import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;
 import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
 import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
@@ -4283,6 +4286,8 @@ public class ServerHttpSecurity {
 
 
 			private Supplier<ReactiveOpaqueTokenIntrospector> introspector;
 			private Supplier<ReactiveOpaqueTokenIntrospector> introspector;
 
 
+			private Supplier<ReactiveOpaqueTokenAuthenticationConverter> authenticationConverter;
+
 			private OpaqueTokenSpec() {
 			private OpaqueTokenSpec() {
 			}
 			}
 
 
@@ -4321,6 +4326,13 @@ public class ServerHttpSecurity {
 				return this;
 				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
 			 * Allows method chaining to continue configuring the
 			 * {@link ServerHttpSecurity}
 			 * {@link ServerHttpSecurity}
@@ -4331,7 +4343,11 @@ public class ServerHttpSecurity {
 			}
 			}
 
 
 			protected ReactiveAuthenticationManager getAuthenticationManager() {
 			protected ReactiveAuthenticationManager getAuthenticationManager() {
-				return new OpaqueTokenReactiveAuthenticationManager(getIntrospector());
+				final OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager(
+						getIntrospector());
+				Optional.ofNullable(getAuthenticationConverter())
+						.ifPresent(authenticationManager::setAuthenticationConverter);
+				return authenticationManager;
 			}
 			}
 
 
 			protected ReactiveOpaqueTokenIntrospector getIntrospector() {
 			protected ReactiveOpaqueTokenIntrospector getIntrospector() {
@@ -4341,6 +4357,18 @@ public class ServerHttpSecurity {
 				return getBean(ReactiveOpaqueTokenIntrospector.class);
 				return getBean(ReactiveOpaqueTokenIntrospector.class);
 			}
 			}
 
 
+			protected ReactiveOpaqueTokenAuthenticationConverter getAuthenticationConverter() {
+				if (this.authenticationConverter != null) {
+					return this.authenticationConverter.get();
+				}
+				try {
+					return getBean(ReactiveOpaqueTokenAuthenticationConverter.class);
+				}
+				catch (NoSuchBeanDefinitionException nsbde) {
+					return null;
+				}
+			}
+
 			protected void configure(ServerHttpSecurity http) {
 			protected void configure(ServerHttpSecurity http) {
 				ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
 				ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
 				AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
 				AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);

+ 11 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt

@@ -16,6 +16,7 @@
 
 
 package org.springframework.security.config.web.server
 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
 import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector
 
 
 /**
 /**
@@ -30,6 +31,7 @@ import org.springframework.security.oauth2.server.resource.introspection.Reactiv
 class ServerOpaqueTokenDsl {
 class ServerOpaqueTokenDsl {
     private var _introspectionUri: String? = null
     private var _introspectionUri: String? = null
     private var _introspector: ReactiveOpaqueTokenIntrospector? = null
     private var _introspector: ReactiveOpaqueTokenIntrospector? = null
+    private var _authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? = null
     private var clientCredentials: Pair<String, String>? = null
     private var clientCredentials: Pair<String, String>? = null
 
 
     var introspectionUri: String?
     var introspectionUri: String?
@@ -37,14 +39,21 @@ class ServerOpaqueTokenDsl {
         set(value) {
         set(value) {
             _introspectionUri = value
             _introspectionUri = value
             _introspector = null
             _introspector = null
+            _authenticationConverter = null
         }
         }
     var introspector: ReactiveOpaqueTokenIntrospector?
     var introspector: ReactiveOpaqueTokenIntrospector?
         get() = _introspector
         get() = _introspector
         set(value) {
         set(value) {
             _introspector = value
             _introspector = value
+            _authenticationConverter = null
             _introspectionUri = null
             _introspectionUri = null
             clientCredentials = null
             clientCredentials = null
         }
         }
+    var authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter?
+        get() = _authenticationConverter
+        set(value) {
+            _authenticationConverter = value
+        }
 
 
     /**
     /**
      * Configures the credentials for Introspection endpoint.
      * Configures the credentials for Introspection endpoint.
@@ -55,6 +64,7 @@ class ServerOpaqueTokenDsl {
     fun introspectionClientCredentials(clientId: String, clientSecret: String) {
     fun introspectionClientCredentials(clientId: String, clientSecret: String) {
         clientCredentials = Pair(clientId, clientSecret)
         clientCredentials = Pair(clientId, clientSecret)
         _introspector = null
         _introspector = null
+        _authenticationConverter = null
     }
     }
 
 
     internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit {
     internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit {
@@ -62,6 +72,7 @@ class ServerOpaqueTokenDsl {
             introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) }
             introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) }
             clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) }
             clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) }
             introspector?.also { opaqueToken.introspector(introspector) }
             introspector?.also { opaqueToken.introspector(introspector) }
+            authenticationConverter?.also { opaqueToken.authenticationConverter(authenticationConverter) }
         }
         }
     }
     }
 }
 }

+ 8 - 0
config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt

@@ -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.builders.HttpSecurity
 import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
 import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
 import org.springframework.security.core.Authentication
 import org.springframework.security.core.Authentication
+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.OpaqueTokenIntrospector
 
 
 /**
 /**
@@ -37,6 +38,7 @@ class OpaqueTokenDsl {
     private var _introspectionUri: String? = null
     private var _introspectionUri: String? = null
     private var _introspector: OpaqueTokenIntrospector? = null
     private var _introspector: OpaqueTokenIntrospector? = null
     private var clientCredentials: Pair<String, String>? = null
     private var clientCredentials: Pair<String, String>? = null
+    private var _authenticationConverter: OpaqueTokenAuthenticationConverter? = null
 
 
     var authenticationManager: AuthenticationManager? = null
     var authenticationManager: AuthenticationManager? = null
 
 
@@ -54,6 +56,11 @@ class OpaqueTokenDsl {
             clientCredentials = null
             clientCredentials = null
         }
         }
 
 
+    var authenticationConverter: OpaqueTokenAuthenticationConverter?
+        get() = _authenticationConverter
+        set(value) {
+            _authenticationConverter = value
+        }
 
 
     /**
     /**
      * Configures the credentials for Introspection endpoint.
      * Configures the credentials for Introspection endpoint.
@@ -70,6 +77,7 @@ class OpaqueTokenDsl {
         return { opaqueToken ->
         return { opaqueToken ->
             introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) }
             introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) }
             introspector?.also { opaqueToken.introspector(introspector) }
             introspector?.also { opaqueToken.introspector(introspector) }
+            authenticationConverter?.also { opaqueToken.authenticationConverter(authenticationConverter) }
             clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) }
             clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) }
             authenticationManager?.also { opaqueToken.authenticationManager(authenticationManager) }
             authenticationManager?.also { opaqueToken.authenticationManager(authenticationManager) }
         }
         }

+ 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 &=
 opaque-token.attlist &=
     ## Reference to an OpaqueTokenIntrospector
     ## Reference to an OpaqueTokenIntrospector
     attribute introspector-ref {xsd:token}?
     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 =
 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>.
 	## 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:documentation>
          </xs:annotation>
          </xs:annotation>
       </xs:attribute>
       </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:attributeGroup>
   
   
   <xs:element name="attribute-exchange">
   <xs:element name="attribute-exchange">

+ 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.Clock;
 import java.time.Instant;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZoneId;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
@@ -68,13 +69,17 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.MediaType;
 import org.springframework.http.RequestEntity;
 import org.springframework.http.RequestEntity;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.JwtBeanDefinitionParser;
 import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.JwtBeanDefinitionParser;
 import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.OpaqueTokenBeanDefinitionParser;
 import org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParser.OpaqueTokenBeanDefinitionParser;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
@@ -87,6 +92,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.oauth2.jwt.TestJwts;
 import org.springframework.security.oauth2.jwt.TestJwts;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 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.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.introspection.OpaqueTokenIntrospector;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
 import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
 import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
@@ -662,6 +668,20 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
 		// @formatter:on
 		// @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
 	@Test
 	public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
 	public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
 		this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
 		this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
@@ -1096,4 +1116,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());
+		}
+
+	}
+
 }
 }

+ 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

@@ -1325,6 +1325,10 @@ The Client Id to use for client authentication against the provided `introspecti
 * **client-secret**
 * **client-secret**
 The Client Secret to use for client authentication against the provided `introspection-uri`.
 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]]
 [[nsa-relying-party-registrations]]
 == <relying-party-registrations>
 == <relying-party-registrations>

+ 4 - 2
docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc

@@ -295,11 +295,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()`.
 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:
 The filter chain is specified like so:
 
 

+ 62 - 17
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java

@@ -17,7 +17,6 @@
 package org.springframework.security.oauth2.server.resource.authentication;
 package org.springframework.security.oauth2.server.resource.authentication;
 
 
 import java.time.Instant;
 import java.time.Instant;
-import java.util.Collection;
 
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.commons.logging.LogFactory;
@@ -25,6 +24,7 @@ import org.apache.commons.logging.LogFactory;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.GrantedAuthority;
@@ -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.InvalidBearerTokenException;
 import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException;
 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.OAuth2IntrospectionException;
+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.OpaqueTokenIntrospector;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
@@ -49,16 +50,21 @@ import org.springframework.util.Assert;
  * opaque access token, returning its attributes set as part of the {@link Authentication}
  * opaque access token, returning its attributes set as part of the {@link Authentication}
  * statement.
  * statement.
  * <p>
  * <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>
+ * This {@link ReactiveAuthenticationManager} is responsible for introspecting and
+ * verifying an opaque access token, returning its attributes set as part of the
+ * {@link Authentication} statement.
+ * <p>
+ * <p>
+ * {@link org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector}
+ * is responsible for retrieving token attributes from authorization-server.
+ * </p>
+ * <p>
+ * authenticationConverter is responsible for turning successful introspection into
+ * {@link Authentication} (which includes {@link GrantedAuthority}s mapping from token
+ * attributes or retrieving from an other source)
  *
  *
  * @author Josh Cummings
  * @author Josh Cummings
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
  * @since 5.2
  * @since 5.2
  * @see AuthenticationProvider
  * @see AuthenticationProvider
  */
  */
@@ -68,6 +74,8 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 
 
 	private final OpaqueTokenIntrospector introspector;
 	private final OpaqueTokenIntrospector introspector;
 
 
+	private OpaqueTokenAuthenticationConverter authenticationConverter;
+
 	/**
 	/**
 	 * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
 	 * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
 	 * @param introspector The {@link OpaqueTokenIntrospector} to use
 	 * @param introspector The {@link OpaqueTokenIntrospector} to use
@@ -75,12 +83,20 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 	public OpaqueTokenAuthenticationProvider(OpaqueTokenIntrospector introspector) {
 	public OpaqueTokenAuthenticationProvider(OpaqueTokenIntrospector introspector) {
 		Assert.notNull(introspector, "introspector cannot be null");
 		Assert.notNull(introspector, "introspector cannot be null");
 		this.introspector = introspector;
 		this.introspector = introspector;
+		this.setAuthenticationConverter(OpaqueTokenAuthenticationProvider::convert);
 	}
 	}
 
 
 	/**
 	/**
+	 * <p>
 	 * Introspect and validate the opaque
 	 * Introspect and validate the opaque
 	 * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer
 	 * <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>
+	 * <p>
+	 * If created Authentication is instance of {@link AbstractAuthenticationToken} and
+	 * details are null, then introspection result details are used.
+	 * </p>
 	 * @param authentication the authentication request object.
 	 * @param authentication the authentication request object.
 	 * @return A successful authentication
 	 * @return A successful authentication
 	 * @throws AuthenticationException if authentication failed for some reason
 	 * @throws AuthenticationException if authentication failed for some reason
@@ -92,8 +108,16 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 		}
 		}
 		BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
 		BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
 		OAuth2AuthenticatedPrincipal principal = getOAuth2AuthenticatedPrincipal(bearer);
 		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");
 		this.logger.debug("Authenticated token");
 		return result;
 		return result;
 	}
 	}
@@ -116,11 +140,32 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 		return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
 		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 sring that was successfuly introspected
+	 * @param authenticatedPrincipal the successful introspection output
+	 * @returna {@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;
 	}
 	}
 
 
 }
 }

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

@@ -16,28 +16,27 @@
 
 
 package org.springframework.security.oauth2.server.resource.authentication;
 package org.springframework.security.oauth2.server.resource.authentication;
 
 
-import java.time.Instant;
-import java.util.Collection;
-
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 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.BearerTokenAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
 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.BadOpaqueTokenException;
 import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
 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.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
 /**
 /**
  * An {@link ReactiveAuthenticationManager} implementation for opaque
  * An {@link ReactiveAuthenticationManager} implementation for opaque
- * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer
+ * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target= "_blank">Bearer
  * Token</a>s, using an
  * Token</a>s, using an
  * <a href="https://tools.ietf.org/html/rfc7662" target="_blank">OAuth 2.0 Introspection
  * <a href="https://tools.ietf.org/html/rfc7662" target="_blank">OAuth 2.0 Introspection
  * Endpoint</a> to check the token's validity and reveal its attributes.
  * Endpoint</a> to check the token's validity and reveal its attributes.
@@ -46,16 +45,17 @@ import org.springframework.util.Assert;
  * verifying an opaque access token, returning its attributes set as part of the
  * verifying an opaque access token, returning its attributes set as part of the
  * {@link Authentication} statement.
  * {@link Authentication} statement.
  * <p>
  * <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>
+ * <p>
+ * {@link ReactiveOpaqueTokenIntrospector} is responsible for retrieving token attributes
+ * from authorization-server.
+ * </p>
+ * <p>
+ * authenticationConverter is responsible for turning successful introspection into
+ * {@link Authentication} (which includes {@link GrantedAuthority}s mapping from token
+ * attributes or retrieving from another source)
  *
  *
  * @author Josh Cummings
  * @author Josh Cummings
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
  * @since 5.2
  * @since 5.2
  * @see ReactiveAuthenticationManager
  * @see ReactiveAuthenticationManager
  */
  */
@@ -63,6 +63,8 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 
 
 	private final ReactiveOpaqueTokenIntrospector introspector;
 	private final ReactiveOpaqueTokenIntrospector introspector;
 
 
+	private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter;
+
 	/**
 	/**
 	 * Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
 	 * Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
 	 * parameters
 	 * parameters
@@ -71,8 +73,23 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 	public OpaqueTokenReactiveAuthenticationManager(ReactiveOpaqueTokenIntrospector introspector) {
 	public OpaqueTokenReactiveAuthenticationManager(ReactiveOpaqueTokenIntrospector introspector) {
 		Assert.notNull(introspector, "introspector cannot be null");
 		Assert.notNull(introspector, "introspector cannot be null");
 		this.introspector = introspector;
 		this.introspector = introspector;
+		this.setAuthenticationConverter(OpaqueTokenReactiveAuthenticationManager::convert);
 	}
 	}
 
 
+	/**
+	 * <p>
+	 * 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 OpaqueTokenAuthenticationConverter}.
+	 * </p>
+	 * <p>
+	 * If created Authentication is instance of {@link AbstractAuthenticationToken} and
+	 * details are null, then introspection result details are used.
+	 * </p>
+	 * @param authentication the authentication request object.
+	 * @return A successful authentication
+	 */
 	@Override
 	@Override
 	public Mono<Authentication> authenticate(Authentication authentication) {
 	public Mono<Authentication> authenticate(Authentication authentication) {
 		// @formatter:off
 		// @formatter:off
@@ -80,21 +97,14 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 				.filter(BearerTokenAuthenticationToken.class::isInstance)
 				.filter(BearerTokenAuthenticationToken.class::isInstance)
 				.cast(BearerTokenAuthenticationToken.class)
 				.cast(BearerTokenAuthenticationToken.class)
 				.map(BearerTokenAuthenticationToken::getToken)
 				.map(BearerTokenAuthenticationToken::getToken)
-				.flatMap(this::authenticate)
-				.cast(Authentication.class);
+				.flatMap(this::authenticate);
 		// @formatter:on
 		// @formatter:on
 	}
 	}
 
 
-	private Mono<BearerTokenAuthentication> authenticate(String token) {
+	private Mono<Authentication> authenticate(String token) {
 		// @formatter:off
 		// @formatter:off
 		return this.introspector.introspect(token)
 		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);
 				.onErrorMap(OAuth2IntrospectionException.class, this::onError);
 		// @formatter:on
 		// @formatter:on
 	}
 	}
@@ -106,4 +116,27 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 		return new AuthenticationServiceException(ex.getMessage(), ex);
 		return new AuthenticationServiceException(ex.getMessage(), ex);
 	}
 	}
 
 
+	/**
+	 * Default reactive {@link OpaqueTokenAuthenticationConverter}.
+	 * @param introspectedToken the bearer sring that was successfuly introspected
+	 * @param authenticatedPrincipal the successful introspection output
+	 * @returna 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;
+	}
+
 }
 }

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

@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Turn successful introspection result into an Authentication instance
+ *
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
+ * @since 5.8
+ */
+@FunctionalInterface
+public interface OpaqueTokenAuthenticationConverter {
+
+	Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal);
+
+}

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

@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Turn successful introspection result into an Authentication instance
+ *
+ * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
+ * @since 5.8
+ */
+@FunctionalInterface
+public interface ReactiveOpaqueTokenAuthenticationConverter {
+
+	Mono<Authentication> convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal);
+
+}

+ 1 - 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");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.