ソースを参照

Polish gh-11665

Steve Riesenberg 3 年 前
コミット
355ef21117
14 ファイル変更258 行追加88 行削除
  1. 12 13
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
  2. 2 2
      config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java
  3. 10 15
      config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
  4. 2 10
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt
  5. 2 7
      config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt
  6. 43 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java
  7. 60 0
      config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java
  8. 14 1
      docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc
  9. 18 19
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java
  10. 13 18
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java
  11. 7 1
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java
  12. 7 1
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java
  13. 33 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java
  14. 35 1
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java

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

@@ -21,12 +21,10 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.Optional;
 import java.util.function.Supplier;
 
 import javax.servlet.http.HttpServletRequest;
 
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.context.ApplicationContext;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.MediaType;
@@ -460,7 +458,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 
 		private Supplier<OpaqueTokenIntrospector> introspector;
 
-		private Supplier<OpaqueTokenAuthenticationConverter> authenticationConverter;
+		private OpaqueTokenAuthenticationConverter authenticationConverter;
 
 		OpaqueTokenConfigurer(ApplicationContext context) {
 			this.context = context;
@@ -499,7 +497,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 		public OpaqueTokenConfigurer authenticationConverter(
 				OpaqueTokenAuthenticationConverter authenticationConverter) {
 			Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
-			this.authenticationConverter = () -> authenticationConverter;
+			this.authenticationConverter = authenticationConverter;
 			return this;
 		}
 
@@ -510,16 +508,14 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 			return this.context.getBean(OpaqueTokenIntrospector.class);
 		}
 
-		Optional<OpaqueTokenAuthenticationConverter> getAuthenticationConverter() {
+		OpaqueTokenAuthenticationConverter getAuthenticationConverter() {
 			if (this.authenticationConverter != null) {
-				return Optional.of(this.authenticationConverter.get());
+				return this.authenticationConverter;
 			}
-			try {
-				return Optional.of(this.context.getBean(OpaqueTokenAuthenticationConverter.class));
-			}
-			catch (NoSuchBeanDefinitionException nsbde) {
-				return Optional.empty();
+			if (this.context.getBeanNamesForType(OpaqueTokenAuthenticationConverter.class).length > 0) {
+				return this.context.getBean(OpaqueTokenAuthenticationConverter.class);
 			}
+			return null;
 		}
 
 		AuthenticationProvider getAuthenticationProvider() {
@@ -527,9 +523,12 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 				return null;
 			}
 			OpaqueTokenIntrospector introspector = getIntrospector();
-			final OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider(
+			OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider(
 					introspector);
-			getAuthenticationConverter().ifPresent(opaqueTokenAuthenticationProvider::setAuthenticationConverter);
+			OpaqueTokenAuthenticationConverter authenticationConverter = getAuthenticationConverter();
+			if (authenticationConverter != null) {
+				opaqueTokenAuthenticationProvider.setAuthenticationConverter(authenticationConverter);
+			}
 			return opaqueTokenAuthenticationProvider;
 		}
 

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

@@ -252,6 +252,7 @@ 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() {
@@ -266,8 +267,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
 					.rootBeanDefinition(OpaqueTokenAuthenticationProvider.class);
 			opaqueTokenProviderBuilder.addConstructorArgValue(introspector);
 			if (StringUtils.hasText(authenticationConverterRef)) {
-				opaqueTokenProviderBuilder.addPropertyValue(AUTHENTICATION_CONVERTER,
-						new RuntimeBeanReference(authenticationConverterRef));
+				opaqueTokenProviderBuilder.addPropertyReference(AUTHENTICATION_CONVERTER, authenticationConverterRef);
 			}
 			return opaqueTokenProviderBuilder.getBeanDefinition();
 		}

+ 10 - 15
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.
@@ -27,7 +27,6 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.UUID;
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -36,7 +35,6 @@ import reactor.core.publisher.Mono;
 import reactor.util.context.Context;
 
 import org.springframework.beans.BeansException;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.context.ApplicationContext;
 import org.springframework.core.Ordered;
 import org.springframework.core.ResolvableType;
@@ -4286,7 +4284,7 @@ public class ServerHttpSecurity {
 
 			private Supplier<ReactiveOpaqueTokenIntrospector> introspector;
 
-			private Supplier<ReactiveOpaqueTokenAuthenticationConverter> authenticationConverter;
+			private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter;
 
 			private OpaqueTokenSpec() {
 			}
@@ -4329,7 +4327,7 @@ public class ServerHttpSecurity {
 			public OpaqueTokenSpec authenticationConverter(
 					ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) {
 				Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
-				this.authenticationConverter = () -> authenticationConverter;
+				this.authenticationConverter = authenticationConverter;
 				return this;
 			}
 
@@ -4343,10 +4341,12 @@ public class ServerHttpSecurity {
 			}
 
 			protected ReactiveAuthenticationManager getAuthenticationManager() {
-				final OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager(
+				OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager(
 						getIntrospector());
-				Optional.ofNullable(getAuthenticationConverter())
-						.ifPresent(authenticationManager::setAuthenticationConverter);
+				ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = getAuthenticationConverter();
+				if (authenticationConverter != null) {
+					authenticationManager.setAuthenticationConverter(authenticationConverter);
+				}
 				return authenticationManager;
 			}
 
@@ -4359,14 +4359,9 @@ public class ServerHttpSecurity {
 
 			protected ReactiveOpaqueTokenAuthenticationConverter getAuthenticationConverter() {
 				if (this.authenticationConverter != null) {
-					return this.authenticationConverter.get();
-				}
-				try {
-					return getBean(ReactiveOpaqueTokenAuthenticationConverter.class);
-				}
-				catch (NoSuchBeanDefinitionException nsbde) {
-					return null;
+					return this.authenticationConverter;
 				}
+				return getBeanOrNull(ReactiveOpaqueTokenAuthenticationConverter.class);
 			}
 
 			protected void configure(ServerHttpSecurity http) {

+ 2 - 10
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.
@@ -31,7 +31,6 @@ import org.springframework.security.oauth2.server.resource.introspection.Reactiv
 class ServerOpaqueTokenDsl {
     private var _introspectionUri: String? = null
     private var _introspector: ReactiveOpaqueTokenIntrospector? = null
-    private var _authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? = null
     private var clientCredentials: Pair<String, String>? = null
 
     var introspectionUri: String?
@@ -39,21 +38,15 @@ class ServerOpaqueTokenDsl {
         set(value) {
             _introspectionUri = value
             _introspector = null
-            _authenticationConverter = null
         }
     var introspector: ReactiveOpaqueTokenIntrospector?
         get() = _introspector
         set(value) {
             _introspector = value
-            _authenticationConverter = null
             _introspectionUri = null
             clientCredentials = null
         }
-    var authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter?
-        get() = _authenticationConverter
-        set(value) {
-            _authenticationConverter = value
-        }
+    var authenticationConverter: ReactiveOpaqueTokenAuthenticationConverter? = null
 
     /**
      * Configures the credentials for Introspection endpoint.
@@ -64,7 +57,6 @@ class ServerOpaqueTokenDsl {
     fun introspectionClientCredentials(clientId: String, clientSecret: String) {
         clientCredentials = Pair(clientId, clientSecret)
         _introspector = null
-        _authenticationConverter = null
     }
 
     internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit {

+ 2 - 7
config/src/main/kotlin/org/springframework/security/config/web/servlet/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.
@@ -38,7 +38,6 @@ class OpaqueTokenDsl {
     private var _introspectionUri: String? = null
     private var _introspector: OpaqueTokenIntrospector? = null
     private var clientCredentials: Pair<String, String>? = null
-    private var _authenticationConverter: OpaqueTokenAuthenticationConverter? = null
 
     var authenticationManager: AuthenticationManager? = null
 
@@ -56,11 +55,7 @@ class OpaqueTokenDsl {
             clientCredentials = null
         }
 
-    var authenticationConverter: OpaqueTokenAuthenticationConverter?
-        get() = _authenticationConverter
-        set(value) {
-            _authenticationConverter = value
-        }
+    var authenticationConverter: OpaqueTokenAuthenticationConverter? = null
 
     /**
      * Configures the credentials for Introspection endpoint.

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

@@ -82,6 +82,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.SecurityContextChangedListenerConfig;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@@ -103,6 +104,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;
@@ -121,6 +123,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;
@@ -1387,6 +1390,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));
 	}
@@ -2441,6 +2460,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 {
 

+ 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;
@@ -567,6 +571,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
@@ -1037,6 +1060,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 {
 

+ 14 - 1
docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc

@@ -313,7 +313,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 +336,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()`
 

+ 18 - 19
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.
@@ -17,6 +17,7 @@
 package org.springframework.security.oauth2.server.resource.authentication;
 
 import java.time.Instant;
+import java.util.Collection;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -24,7 +25,6 @@ import org.apache.commons.logging.LogFactory;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationProvider;
 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;
@@ -50,18 +50,21 @@ import org.springframework.util.Assert;
  * opaque access token, returning its attributes set as part of the {@link Authentication}
  * statement.
  * <p>
- * This {@link ReactiveAuthenticationManager} is responsible for introspecting and
- * verifying an opaque access token, returning its attributes set as part of the
- * {@link Authentication} statement.
+ * 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>
+ * An {@link OpaqueTokenIntrospector} is responsible for retrieving token attributes from
+ * an authorization server.
  * <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)
+ * 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;
@@ -74,7 +77,7 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 
 	private final OpaqueTokenIntrospector introspector;
 
-	private OpaqueTokenAuthenticationConverter authenticationConverter;
+	private OpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenAuthenticationProvider::convert;
 
 	/**
 	 * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
@@ -83,20 +86,16 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 	public OpaqueTokenAuthenticationProvider(OpaqueTokenIntrospector introspector) {
 		Assert.notNull(introspector, "introspector cannot be null");
 		this.introspector = introspector;
-		this.setAuthenticationConverter(OpaqueTokenAuthenticationProvider::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
 	 * @throws AuthenticationException if authentication failed for some reason
@@ -142,9 +141,9 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 
 	/**
 	 * Default {@link OpaqueTokenAuthenticationConverter}.
-	 * @param introspectedToken the bearer sring that was successfuly introspected
+	 * @param introspectedToken the bearer string that was successfully introspected
 	 * @param authenticatedPrincipal the successful introspection output
-	 * @returna {@link BearerTokenAuthentication}
+	 * @return a {@link BearerTokenAuthentication}
 	 */
 	static BearerTokenAuthentication convert(String introspectedToken,
 			OAuth2AuthenticatedPrincipal authenticatedPrincipal) {

+ 13 - 18
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.
@@ -36,7 +36,7 @@ import org.springframework.util.Assert;
 
 /**
  * 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
  * <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.
@@ -45,14 +45,13 @@ import org.springframework.util.Assert;
  * verifying an opaque access token, returning its attributes set as part of the
  * {@link Authentication} statement.
  * <p>
+ * A {@link ReactiveOpaqueTokenIntrospector} is responsible for retrieving token
+ * attributes from an authorization server.
  * <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)
+ * 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;
@@ -63,7 +62,7 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 
 	private final ReactiveOpaqueTokenIntrospector introspector;
 
-	private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter;
+	private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = OpaqueTokenReactiveAuthenticationManager::convert;
 
 	/**
 	 * Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
@@ -73,20 +72,16 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 	public OpaqueTokenReactiveAuthenticationManager(ReactiveOpaqueTokenIntrospector introspector) {
 		Assert.notNull(introspector, "introspector cannot be null");
 		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>
+	 * {@link ReactiveOpaqueTokenAuthenticationConverter}.
 	 * <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
 	 */
@@ -117,10 +112,10 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
 	}
 
 	/**
-	 * Default reactive {@link OpaqueTokenAuthenticationConverter}.
-	 * @param introspectedToken the bearer sring that was successfuly introspected
+	 * Default {@link ReactiveOpaqueTokenAuthenticationConverter}.
+	 * @param introspectedToken the bearer string that was successfully introspected
 	 * @param authenticatedPrincipal the successful introspection output
-	 * @returna an async wrapper of default {@link OpaqueTokenAuthenticationConverter}
+	 * @return an async wrapper of default {@link OpaqueTokenAuthenticationConverter}
 	 * result
 	 */
 	static Mono<Authentication> convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) {

+ 7 - 1
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OpaqueTokenAuthenticationConverter.java

@@ -20,7 +20,7 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 
 /**
- * Turn successful introspection result into an Authentication instance
+ * Convert a successful introspection result into an authentication result.
  *
  * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
  * @since 5.8
@@ -28,6 +28,12 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 @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);
 
 }

+ 7 - 1
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/ReactiveOpaqueTokenAuthenticationConverter.java

@@ -22,7 +22,7 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 
 /**
- * Turn successful introspection result into an Authentication instance
+ * Convert a successful introspection result into an authentication result.
  *
  * @author Jerome Wacongne &lt;ch4mp@c4-soft.com&gt;
  * @since 5.8
@@ -30,6 +30,12 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 @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);
 
 }

+ 33 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java

@@ -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);
+	}
+
 }