Bläddra i källkod

Separate Resource Server Reactive Docs

Issue gh-10367
Josh Cummings 3 år sedan
förälder
incheckning
6d2d3b9a69

+ 5 - 1
docs/modules/ROOT/nav.adoc

@@ -109,7 +109,11 @@
 ** xref:reactive/oauth2/index.adoc[OAuth2]
 *** xref:reactive/oauth2/login.adoc[OAuth2 Log In]
 *** xref:reactive/oauth2/oauth2-client.adoc[OAuth2 Client]
-*** xref:reactive/oauth2/resource-server.adoc[OAuth2 Resource Server]
+*** xref:reactive/oauth2/resource-server/index.adoc[OAuth2 Resource Server]
+**** xref:reactive/oauth2/resource-server/jwt.adoc[JWT]
+**** xref:reactive/oauth2/resource-server/opaque-token.adoc[Opaque Token]
+**** xref:reactive/oauth2/resource-server/multitenancy.adoc[Multitenancy]
+**** xref:reactive/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens]
 ** xref:reactive/exploits/index.adoc[Protection Against Exploits]
 *** xref:reactive/exploits/csrf.adoc[CSRF]
 *** xref:reactive/exploits/headers.adoc[Headers]

+ 1 - 1
docs/modules/ROOT/pages/reactive/oauth2/index.adoc

@@ -5,4 +5,4 @@ Spring Security provides OAuth2 and WebFlux integration for reactive application
 
 * xref:reactive/oauth2/login.adoc[OAuth2 Log In] - Authenticating with an OAuth2 or OpenID Connect 1.0 Provider
 * xref:reactive/oauth2/oauth2-client.adoc[OAuth2 Client] - Making requests to an OAuth2 Resource Server
-* xref:reactive/oauth2/resource-server.adoc[OAuth2 Resource Server] - Protecting a REST endpoint using OAuth2
+* xref:reactive/oauth2/resource-server/index.adoc[OAuth2 Resource Server] - Protecting a REST endpoint using OAuth2

+ 0 - 1907
docs/modules/ROOT/pages/reactive/oauth2/resource-server.adoc

@@ -1,1907 +0,0 @@
-[[webflux-oauth2-resource-server]]
-= OAuth 2.0 Resource Server
-
-Spring Security supports protecting endpoints using two forms of OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]:
-
-* https://tools.ietf.org/html/rfc7519[JWT]
-* Opaque Tokens
-
-This is handy in circumstances where an application has delegated its authority management to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity).
-This authorization server can be consulted by resource servers to authorize requests.
-
-[NOTE]
-====
-A complete working example for {gh-samples-url}/reactive/webflux/java/oauth2/resource-server[*JWTs*]  is available in the {gh-samples-url}[Spring Security repository].
-====
-
-[[webflux-oauth2resourceserver-jwt-minimaldependencies]]
-== Minimal Dependencies for JWT
-
-Most Resource Server support is collected into `spring-security-oauth2-resource-server`.
-However, the support for decoding and verifying JWTs is in `spring-security-oauth2-jose`, meaning that both are necessary in order to have a working resource server that supports JWT-encoded Bearer Tokens.
-
-[[webflux-oauth2resourceserver-jwt-minimalconfiguration]]
-== Minimal Configuration for JWTs
-
-When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a resource server consists of two basic steps.
-First, include the needed dependencies and second, indicate the location of the authorization server.
-
-=== Specifying the Authorization Server
-
-In a Spring Boot application, to specify which authorization server to use, simply do:
-
-[source,yml]
-----
-spring:
-  security:
-    oauth2:
-      resourceserver:
-        jwt:
-          issuer-uri: https://idp.example.com/issuer
-----
-
-Where `https://idp.example.com/issuer` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
-Resource Server will use this property to further self-configure, discover the authorization server's public keys, and subsequently validate incoming JWTs.
-
-[NOTE]
-To use the `issuer-uri` property, it must also be true that one of `https://idp.example.com/issuer/.well-known/openid-configuration`, `https://idp.example.com/.well-known/openid-configuration/issuer`, or `https://idp.example.com/.well-known/oauth-authorization-server/issuer` is a supported endpoint for the authorization server.
-This endpoint is referred to as a https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Provider Configuration] endpoint or a https://tools.ietf.org/html/rfc8414#section-3[Authorization Server Metadata] endpoint.
-
-And that's it!
-
-=== Startup Expectations
-
-When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
-
-It achieves this through a deterministic startup process:
-
-1. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property
-2. Configure the validation strategy to query `jwks_url` for valid public keys
-3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
-
-A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
-
-[NOTE]
-If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.
-
-=== Runtime Expectations
-
-Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
-
-[source,html]
-----
-GET / HTTP/1.1
-Authorization: Bearer some-token-value # Resource Server will process this
-----
-
-So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
-
-Given a well-formed JWT, Resource Server will:
-
-1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
-2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
-3. Map each scope to an authority with the prefix `SCOPE_`.
-
-[NOTE]
-As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
-
-The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
-
-From here, consider jumping to:
-
-<<webflux-oauth2resourceserver-jwt-jwkseturi,How to Configure without Tying Resource Server startup to an authorization server's availability>>
-
-<<webflux-oauth2resourceserver-jwt-sansboot,How to Configure without Spring Boot>>
-
-[[webflux-oauth2resourceserver-jwt-jwkseturi]]
-=== Specifying the Authorization Server JWK Set Uri Directly
-
-If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
-
-[source,yaml]
-----
-spring:
-  security:
-    oauth2:
-      resourceserver:
-        jwt:
-          issuer-uri: https://idp.example.com
-          jwk-set-uri: https://idp.example.com/.well-known/jwks.json
-----
-
-[NOTE]
-The JWK Set uri is not standardized, but can typically be found in the authorization server's documentation
-
-Consequently, Resource Server will not ping the authorization server at startup.
-We still specify the `issuer-uri` so that Resource Server still validates the `iss` claim on incoming JWTs.
-
-[NOTE]
-This property can also be supplied directly on the <<webflux-oauth2resourceserver-jwt-jwkseturi-dsl,DSL>>.
-
-[[webflux-oauth2resourceserver-jwt-sansboot]]
-=== Overriding or Replacing Boot Auto Configuration
-
-There are two ``@Bean``s that Spring Boot generates on Resource Server's behalf.
-
-The first is a `SecurityWebFilterChain` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `SecurityWebFilterChain` looks like:
-
-.Resource Server SecurityWebFilterChain
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-	http
-		.authorizeExchange(exchanges -> exchanges
-			.anyExchange().authenticated()
-		)
-		.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
-	return http.build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            jwt { }
-        }
-    }
-}
-----
-====
-
-If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one.
-
-Replacing this is as simple as exposing the bean within the application:
-
-.Replacing SecurityWebFilterChain
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-	http
-		.authorizeExchange(exchanges -> exchanges
-			.pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
-			.anyExchange().authenticated()
-		)
-		.oauth2ResourceServer(oauth2 -> oauth2
-			.jwt(withDefaults())
-		);
-	return http.build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize("/message/**", hasAuthority("SCOPE_message:read"))
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            jwt { }
-        }
-    }
-}
-----
-====
-
-The above requires the scope of `message:read` for any URL that starts with `/messages/`.
-
-Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration.
-
-For example, the second `@Bean` Spring Boot creates is a `ReactiveJwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`:
-
-.ReactiveJwtDecoder
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public ReactiveJwtDecoder jwtDecoder() {
-    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
-}
-----
-====
-
-[NOTE]
-Calling `{security-api-url}org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-[ReactiveJwtDecoders#fromIssuerLocation]` is what invokes the Provider Configuration or Authorization Server Metadata endpoint in order to derive the JWK Set Uri.
-If the application doesn't expose a `ReactiveJwtDecoder` bean, then Spring Boot will expose the above default one.
-
-And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`.
-
-[[webflux-oauth2resourceserver-jwt-jwkseturi-dsl]]
-==== Using `jwkSetUri()`
-
-An authorization server's JWK Set Uri can be configured <<webflux-oauth2resourceserver-jwt-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-	http
-		.authorizeExchange(exchanges -> exchanges
-			.anyExchange().authenticated()
-		)
-		.oauth2ResourceServer(oauth2 -> oauth2
-			.jwt(jwt -> jwt
-				.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
-			)
-		);
-	return http.build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            jwt {
-                jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
-            }
-        }
-    }
-}
-----
-====
-
-Using `jwkSetUri()` takes precedence over any configuration property.
-
-[[webflux-oauth2resourceserver-jwt-decoder-dsl]]
-==== Using `decoder()`
-
-More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-	http
-		.authorizeExchange(exchanges -> exchanges
-			.anyExchange().authenticated()
-		)
-		.oauth2ResourceServer(oauth2 -> oauth2
-			.jwt(jwt -> jwt
-				.decoder(myCustomDecoder())
-			)
-		);
-    return http.build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            jwt {
-                jwtDecoder = myCustomDecoder()
-            }
-        }
-    }
-}
-----
-====
-
-This is handy when deeper configuration, like <<webflux-oauth2resourceserver-jwt-validation,validation>>, is necessary.
-
-[[webflux-oauth2resourceserver-decoder-bean]]
-==== Exposing a `ReactiveJwtDecoder` `@Bean`
-
-Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
-}
-----
-====
-
-[[webflux-oauth2resourceserver-jwt-decoder-algorithm]]
-== Configuring Trusted Algorithms
-
-By default, `NimbusReactiveJwtDecoder`, and hence Resource Server, will only trust and verify tokens using `RS256`.
-
-You can customize this via <<webflux-oauth2resourceserver-jwt-boot-algorithm,Spring Boot>> or <<webflux-oauth2resourceserver-jwt-decoder-builder,the NimbusJwtDecoder builder>>.
-
-[[webflux-oauth2resourceserver-jwt-boot-algorithm]]
-=== Via Spring Boot
-
-The simplest way to set the algorithm is as a property:
-
-[source,yaml]
-----
-spring:
-  security:
-    oauth2:
-      resourceserver:
-        jwt:
-          jws-algorithm: RS512
-          jwk-set-uri: https://idp.example.org/.well-known/jwks.json
-----
-
-[[webflux-oauth2resourceserver-jwt-decoder-builder]]
-=== Using a Builder
-
-For greater power, though, we can use a builder that ships with `NimbusReactiveJwtDecoder`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
-            .jwsAlgorithm(RS512).build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
-            .jwsAlgorithm(RS512).build()
-}
-----
-====
-
-Calling `jwsAlgorithm` more than once will configure `NimbusReactiveJwtDecoder` to trust more than one algorithm, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
-            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
-            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
-}
-----
-====
-
-Or, you can call `jwsAlgorithms`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
-            .jwsAlgorithms(algorithms -> {
-                    algorithms.add(RS512);
-                    algorithms.add(ES512);
-            }).build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
-            .jwsAlgorithms {
-                it.add(RS512)
-                it.add(ES512)
-            }
-            .build()
-}
-----
-====
-
-[[webflux-oauth2resourceserver-jwt-decoder-public-key]]
-=== Trusting a Single Asymmetric Key
-
-Simpler than backing a Resource Server with a JWK Set endpoint is to hard-code an RSA public key.
-The public key can be provided via <<webflux-oauth2resourceserver-jwt-decoder-public-key-boot,Spring Boot>> or by <<webflux-oauth2resourceserver-jwt-decoder-public-key-builder,Using a Builder>>.
-
-[[webflux-oauth2resourceserver-jwt-decoder-public-key-boot]]
-==== Via Spring Boot
-
-Specifying a key via Spring Boot is quite simple.
-The key's location can be specified like so:
-
-[source,yaml]
-----
-spring:
-  security:
-    oauth2:
-      resourceserver:
-        jwt:
-          public-key-location: classpath:my-key.pub
-----
-
-Or, to allow for a more sophisticated lookup, you can post-process the `RsaKeyConversionServicePostProcessor`:
-
-.BeanFactoryPostProcessor
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-BeanFactoryPostProcessor conversionServiceCustomizer() {
-    return beanFactory ->
-        beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
-                .setResourceLoader(new CustomResourceLoader());
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
-    return BeanFactoryPostProcessor { beanFactory: ConfigurableListableBeanFactory ->
-        beanFactory.getBean<RsaKeyConversionServicePostProcessor>()
-                .setResourceLoader(CustomResourceLoader())
-    }
-}
-----
-====
-
-Specify your key's location:
-
-[source,yaml]
-----
-key.location: hfds://my-key.pub
-----
-
-And then autowire the value:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Value("${key.location}")
-RSAPublicKey key;
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Value("\${key.location}")
-val key: RSAPublicKey? = null
-----
-====
-
-[[webflux-oauth2resourceserver-jwt-decoder-public-key-builder]]
-==== Using a Builder
-
-To wire an `RSAPublicKey` directly, you can simply use the appropriate `NimbusReactiveJwtDecoder` builder, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    return NimbusReactiveJwtDecoder.withPublicKey(key).build()
-}
-----
-====
-
-[[webflux-oauth2resourceserver-jwt-decoder-secret-key]]
-=== Trusting a Single Symmetric Key
-
-Using a single symmetric key is also simple.
-You can simply load in your `SecretKey` and use the appropriate `NimbusReactiveJwtDecoder` builder, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public ReactiveJwtDecoder jwtDecoder() {
-    return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    return NimbusReactiveJwtDecoder.withSecretKey(this.key).build()
-}
-----
-====
-
-[[webflux-oauth2resourceserver-jwt-authorization]]
-=== Configuring Authorization
-
-A JWT that is issued from an OAuth 2.0 Authorization Server will typically either have a `scope` or `scp` attribute, indicating the scopes (or authorities) it's been granted, for example:
-
-`{ ..., "scope" : "messages contacts"}`
-
-When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
-
-This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-	http
-		.authorizeExchange(exchanges -> exchanges
-			.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
-			.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
-			.anyExchange().authenticated()
-		)
-		.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
-    return http.build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
-            authorize("/messages/**", hasAuthority("SCOPE_messages"))
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            jwt { }
-        }
-    }
-}
-----
-====
-
-Or similarly with method security:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@PreAuthorize("hasAuthority('SCOPE_messages')")
-public Flux<Message> getMessages(...) {}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@PreAuthorize("hasAuthority('SCOPE_messages')")
-fun getMessages(): Flux<Message> { }
-----
-====
-
-[[webflux-oauth2resourceserver-jwt-authorization-extraction]]
-==== Extracting Authorities Manually
-
-However, there are a number of circumstances where this default is insufficient.
-For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute.
-Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities.
-
-To this end, the DSL exposes `jwtAuthenticationConverter()`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-	http
-		.authorizeExchange(exchanges -> exchanges
-			.anyExchange().authenticated()
-		)
-		.oauth2ResourceServer(oauth2 -> oauth2
-			.jwt(jwt -> jwt
-				.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
-			)
-		);
-	return http.build();
-}
-
-Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
-    JwtAuthenticationConverter jwtAuthenticationConverter =
-            new JwtAuthenticationConverter();
-    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
-            (new GrantedAuthoritiesExtractor());
-    return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            jwt {
-                jwtAuthenticationConverter = grantedAuthoritiesExtractor()
-            }
-        }
-    }
-}
-
-fun grantedAuthoritiesExtractor(): Converter<Jwt, Mono<AbstractAuthenticationToken>> {
-    val jwtAuthenticationConverter = JwtAuthenticationConverter()
-    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor())
-    return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter)
-}
-----
-====
-
-which is responsible for converting a `Jwt` into an `Authentication`.
-As part of its configuration, we can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities.
-
-That final converter might be something like `GrantedAuthoritiesExtractor` below:
-
-====
-.Java
-[source,java,role="primary"]
-----
-static class GrantedAuthoritiesExtractor
-        implements Converter<Jwt, Collection<GrantedAuthority>> {
-
-    public Collection<GrantedAuthority> convert(Jwt jwt) {
-        Collection<?> authorities = (Collection<?>)
-                jwt.getClaims().getOrDefault("mycustomclaim", Collections.emptyList());
-
-        return authorities.stream()
-                .map(Object::toString)
-                .map(SimpleGrantedAuthority::new)
-                .collect(Collectors.toList());
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-internal class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAuthority>> {
-    override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
-        val authorities: List<Any> = jwt.claims
-                .getOrDefault("mycustomclaim", emptyList<Any>()) as List<Any>
-        return authorities
-                .map { it.toString() }
-                .map { SimpleGrantedAuthority(it) }
-    }
-}
-----
-====
-
-For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter<Jwt, Mono<AbstractAuthenticationToken>>`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
-    public AbstractAuthenticationToken convert(Jwt jwt) {
-        return Mono.just(jwt).map(this::doConversion);
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
-    override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
-        return Mono.just(jwt).map(this::doConversion)
-    }
-}
-----
-====
-
-[[webflux-oauth2resourceserver-jwt-validation]]
-=== Configuring Validation
-
-Using <<webflux-oauth2resourceserver-jwt-minimalconfiguration,minimal Spring Boot configuration>>, indicating the authorization server's issuer uri, Resource Server will default to verifying the `iss` claim as well as the `exp` and `nbf` timestamp claims.
-
-In circumstances where validation needs to be customized, Resource Server ships with two standard validators and also accepts custom `OAuth2TokenValidator` instances.
-
-[[webflux-oauth2resourceserver-jwt-validation-clockskew]]
-==== Customizing Timestamp Validation
-
-JWT's typically have a window of validity, with the start of the window indicated in the `nbf` claim and the end indicated in the `exp` claim.
-
-However, every server can experience clock drift, which can cause tokens to appear expired to one server, but not to another.
-This can cause some implementation heartburn as the number of collaborating servers increases in a distributed system.
-
-Resource Server uses `JwtTimestampValidator` to verify a token's validity window, and it can be configured with a `clockSkew` to alleviate the above problem:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-ReactiveJwtDecoder jwtDecoder() {
-     NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
-             ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
-
-     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
-            new JwtTimestampValidator(Duration.ofSeconds(60)),
-            new IssuerValidator(issuerUri));
-
-     jwtDecoder.setJwtValidator(withClockSkew);
-
-     return jwtDecoder;
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
-    val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
-            JwtTimestampValidator(Duration.ofSeconds(60)),
-            JwtIssuerValidator(issuerUri))
-    jwtDecoder.setJwtValidator(withClockSkew)
-    return jwtDecoder
-}
-----
-====
-
-[NOTE]
-By default, Resource Server configures a clock skew of 60 seconds.
-
-[[webflux-oauth2resourceserver-validation-custom]]
-==== Configuring a Custom Validator
-
-Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API:
-
-====
-.Java
-[source,java,role="primary"]
-----
-public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
-    OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
-
-    public OAuth2TokenValidatorResult validate(Jwt jwt) {
-        if (jwt.getAudience().contains("messaging")) {
-            return OAuth2TokenValidatorResult.success();
-        } else {
-            return OAuth2TokenValidatorResult.failure(error);
-        }
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-class AudienceValidator : OAuth2TokenValidator<Jwt> {
-    var error: OAuth2Error = OAuth2Error("invalid_token", "The required audience is missing", null)
-    override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
-        return if (jwt.audience.contains("messaging")) {
-            OAuth2TokenValidatorResult.success()
-        } else {
-            OAuth2TokenValidatorResult.failure(error)
-        }
-    }
-}
-----
-====
-
-Then, to add into a resource server, it's a matter of specifying the `ReactiveJwtDecoder` instance:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-ReactiveJwtDecoder jwtDecoder() {
-    NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
-            ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
-
-    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
-    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
-    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
-
-    jwtDecoder.setJwtValidator(withAudience);
-
-    return jwtDecoder;
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun jwtDecoder(): ReactiveJwtDecoder {
-    val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
-    val audienceValidator: OAuth2TokenValidator<Jwt> = AudienceValidator()
-    val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
-    val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)
-    jwtDecoder.setJwtValidator(withAudience)
-    return jwtDecoder
-}
-----
-====
-
-[[webflux-oauth2resourceserver-opaque-minimaldependencies]]
-=== Minimal Dependencies for Introspection
-As described in xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-minimaldependencies[Minimal Dependencies for JWT] most of Resource Server support is collected in `spring-security-oauth2-resource-server`.
-However unless a custom <<webflux-oauth2resourceserver-opaque-introspector-bean,`ReactiveOpaqueTokenIntrospector`>> is provided, the Resource Server will fallback to ReactiveOpaqueTokenIntrospector.
-Meaning that both `spring-security-oauth2-resource-server` and `oauth2-oidc-sdk` are necessary in order to have a working minimal Resource Server that supports opaque Bearer Tokens.
-Please refer to `spring-security-oauth2-resource-server` in order to determin the correct version for `oauth2-oidc-sdk`.
-
-[[webflux-oauth2resourceserver-opaque-minimalconfiguration]]
-=== Minimal Configuration for Introspection
-
-Typically, an opaque token can be verified via an https://tools.ietf.org/html/rfc7662[OAuth 2.0 Introspection Endpoint], hosted by the authorization server.
-This can be handy when revocation is a requirement.
-
-When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a resource server that uses introspection consists of two basic steps.
-First, include the needed dependencies and second, indicate the introspection endpoint details.
-
-[[webflux-oauth2resourceserver-opaque-introspectionuri]]
-==== Specifying the Authorization Server
-
-To specify where the introspection endpoint is, simply do:
-
-[source,yaml]
-----
-security:
-  oauth2:
-    resourceserver:
-      opaque-token:
-        introspection-uri: https://idp.example.com/introspect
-        client-id: client
-        client-secret: secret
-----
-
-Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint.
-
-Resource Server will use these properties to further self-configure and subsequently validate incoming JWTs.
-
-[NOTE]
-When using introspection, the authorization server's word is the law.
-If the authorization server responses that the token is valid, then it is.
-
-And that's it!
-
-==== Startup Expectations
-
-When this property and these dependencies are used, Resource Server will automatically configure itself to validate Opaque Bearer Tokens.
-
-This startup process is quite a bit simpler than for JWTs since no endpoints need to be discovered and no additional validation rules get added.
-
-==== Runtime Expectations
-
-Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
-
-[source,http]
-----
-GET / HTTP/1.1
-Authorization: Bearer some-token-value # Resource Server will process this
-----
-
-So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
-
-Given an Opaque Token, Resource Server will
-
-1. Query the provided introspection endpoint using the provided credentials and the token
-2. Inspect the response for an `{ 'active' : true }` attribute
-3. Map each scope to an authority with the prefix `SCOPE_`
-
-The resulting `Authentication#getPrincipal`, by default, is a Spring Security `{security-api-url}org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.html[OAuth2AuthenticatedPrincipal]` object, and `Authentication#getName` maps to the token's `sub` property, if one is present.
-
-From here, you may want to jump to:
-
-* <<webflux-oauth2resourceserver-opaque-attributes,Looking Up Attributes Post-Authentication>>
-* <<webflux-oauth2resourceserver-opaque-authorization-extraction,Extracting Authorities Manually>>
-* <<webflux-oauth2resourceserver-opaque-jwt-introspector,Using Introspection with JWTs>>
-
-[[webflux-oauth2resourceserver-opaque-attributes]]
-=== Looking Up Attributes Post-Authentication
-
-Once a token is authenticated, an instance of `BearerTokenAuthentication` is set in the `SecurityContext`.
-
-This means that it's available in `@Controller` methods when using `@EnableWebFlux` in your configuration:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@GetMapping("/foo")
-public Mono<String> foo(BearerTokenAuthentication authentication) {
-    return Mono.just(authentication.getTokenAttributes().get("sub") + " is the subject");
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@GetMapping("/foo")
-fun foo(authentication: BearerTokenAuthentication): Mono<String> {
-    return Mono.just(authentication.tokenAttributes["sub"].toString() + " is the subject")
-}
-----
-====
-
-Since `BearerTokenAuthentication` holds an `OAuth2AuthenticatedPrincipal`, that also means that it's available to controller methods, too:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@GetMapping("/foo")
-public Mono<String> foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
-    return Mono.just(principal.getAttribute("sub") + " is the subject");
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@GetMapping("/foo")
-fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): Mono<String> {
-    return Mono.just(principal.getAttribute<Any>("sub").toString() + " is the subject")
-}
-----
-====
-
-==== Looking Up Attributes Via SpEL
-
-Of course, this also means that attributes can be accessed via SpEL.
-
-For example, if using `@EnableReactiveMethodSecurity` so that you can use `@PreAuthorize` annotations, you can do:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@PreAuthorize("principal?.attributes['sub'] == 'foo'")
-public Mono<String> forFoosEyesOnly() {
-    return Mono.just("foo");
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@PreAuthorize("principal.attributes['sub'] == 'foo'")
-fun forFoosEyesOnly(): Mono<String> {
-    return Mono.just("foo")
-}
-----
-====
-
-[[webflux-oauth2resourceserver-opaque-sansboot]]
-=== Overriding or Replacing Boot Auto Configuration
-
-There are two ``@Bean``s that Spring Boot generates on Resource Server's behalf.
-
-The first is a `SecurityWebFilterChain` that configures the app as a resource server.
-When use Opaque Token, this `SecurityWebFilterChain` looks like:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-	http
-		.authorizeExchange(exchanges -> exchanges
-			.anyExchange().authenticated()
-		)
-		.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken)
-	return http.build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            opaqueToken { }
-        }
-    }
-}
-----
-====
-
-If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one.
-
-Replacing this is as simple as exposing the bean within the application:
-
-.Replacing SecurityWebFilterChain
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebFluxSecurity
-public class MyCustomSecurityConfiguration {
-    @Bean
-    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-        http
-            .authorizeExchange(exchanges -> exchanges
-                .pathMatchers("/messages/**").hasAuthority("SCOPE_message:read")
-                .anyExchange().authenticated()
-            )
-            .oauth2ResourceServer(oauth2 -> oauth2
-                .opaqueToken(opaqueToken -> opaqueToken
-                    .introspector(myIntrospector())
-                )
-            );
-        return http.build();
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize("/messages/**", hasAuthority("SCOPE_message:read"))
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            opaqueToken {
-                introspector = myIntrospector()
-            }
-        }
-    }
-}
-----
-====
-
-The above requires the scope of `message:read` for any URL that starts with `/messages/`.
-
-Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration.
-
-For example, the second `@Bean` Spring Boot creates is a `ReactiveOpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public ReactiveOpaqueTokenIntrospector introspector() {
-    return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun introspector(): ReactiveOpaqueTokenIntrospector {
-    return NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret)
-}
-----
-====
-
-If the application doesn't expose a `ReactiveOpaqueTokenIntrospector` bean, then Spring Boot will expose the above default one.
-
-And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`.
-
-[[webflux-oauth2resourceserver-opaque-introspectionuri-dsl]]
-==== Using `introspectionUri()`
-
-An authorization server's Introspection Uri can be configured <<webflux-oauth2resourceserver-opaque-introspectionuri,as a configuration property>> or it can be supplied in the DSL:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebFluxSecurity
-public class DirectlyConfiguredIntrospectionUri {
-    @Bean
-    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-        http
-            .authorizeExchange(exchanges -> exchanges
-                .anyExchange().authenticated()
-            )
-            .oauth2ResourceServer(oauth2 -> oauth2
-                .opaqueToken(opaqueToken -> opaqueToken
-                    .introspectionUri("https://idp.example.com/introspect")
-                    .introspectionClientCredentials("client", "secret")
-                )
-            );
-        return http.build();
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            opaqueToken {
-                introspectionUri = "https://idp.example.com/introspect"
-                introspectionClientCredentials("client", "secret")
-            }
-        }
-    }
-}
-----
-====
-
-Using `introspectionUri()` takes precedence over any configuration property.
-
-[[webflux-oauth2resourceserver-opaque-introspector-dsl]]
-==== Using `introspector()`
-
-More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of `ReactiveOpaqueTokenIntrospector`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebFluxSecurity
-public class DirectlyConfiguredIntrospector {
-    @Bean
-    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-        http
-            .authorizeExchange(exchanges -> exchanges
-                .anyExchange().authenticated()
-            )
-            .oauth2ResourceServer(oauth2 -> oauth2
-                .opaqueToken(opaqueToken -> opaqueToken
-                    .introspector(myCustomIntrospector())
-                )
-            );
-        return http.build();
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            opaqueToken {
-                introspector = myCustomIntrospector()
-            }
-        }
-    }
-}
-----
-====
-
-This is handy when deeper configuration, like <<webflux-oauth2resourceserver-opaque-authorization-extraction,authority mapping>>or <<webflux-oauth2resourceserver-opaque-jwt-introspector,JWT revocation>> is necessary.
-
-[[webflux-oauth2resourceserver-opaque-introspector-bean]]
-==== Exposing a `ReactiveOpaqueTokenIntrospector` `@Bean`
-
-Or, exposing a `ReactiveOpaqueTokenIntrospector` `@Bean` has the same effect as `introspector()`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public ReactiveOpaqueTokenIntrospector introspector() {
-    return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun introspector(): ReactiveOpaqueTokenIntrospector {
-    return NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret)
-}
-----
-====
-
-[[webflux-oauth2resourceserver-opaque-authorization]]
-=== Configuring Authorization
-
-An OAuth 2.0 Introspection endpoint will typically return a `scope` attribute, indicating the scopes (or authorities) it's been granted, for example:
-
-`{ ..., "scope" : "messages contacts"}`
-
-When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
-
-This means that to protect an endpoint or method with a scope derived from an Opaque Token, the corresponding expressions should include this prefix:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebFluxSecurity
-public class MappedAuthorities {
-    @Bean
-    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-        http
-            .authorizeExchange(exchange -> exchange
-                .pathMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
-                .pathMatchers("/messages/**").hasAuthority("SCOPE_messages")
-                .anyExchange().authenticated()
-            )
-            .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken);
-        return http.build();
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
-    return http {
-        authorizeExchange {
-            authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
-            authorize("/messages/**", hasAuthority("SCOPE_messages"))
-            authorize(anyExchange, authenticated)
-        }
-        oauth2ResourceServer {
-            opaqueToken { }
-        }
-    }
-}
-----
-====
-
-Or similarly with method security:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@PreAuthorize("hasAuthority('SCOPE_messages')")
-public Flux<Message> getMessages(...) {}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@PreAuthorize("hasAuthority('SCOPE_messages')")
-fun getMessages(): Flux<Message> { }
-----
-====
-
-[[webflux-oauth2resourceserver-opaque-authorization-extraction]]
-==== Extracting Authorities Manually
-
-By default, Opaque Token support will extract the scope claim from an introspection response and parse it into individual `GrantedAuthority` instances.
-
-For example, if the introspection response were:
-
-[source,json]
-----
-{
-    "active" : true,
-    "scope" : "message:read message:write"
-}
-----
-
-Then Resource Server would generate an `Authentication` with two authorities, one for `message:read` and the other for `message:write`.
-
-This can, of course, be customized using a custom `ReactiveOpaqueTokenIntrospector` that takes a look at the attribute set and converts in its own way:
-
-====
-.Java
-[source,java,role="primary"]
-----
-public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
-    private ReactiveOpaqueTokenIntrospector delegate =
-            new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
-
-    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
-        return this.delegate.introspect(token)
-                .map(principal -> new DefaultOAuth2AuthenticatedPrincipal(
-                        principal.getName(), principal.getAttributes(), extractAuthorities(principal)));
-    }
-
-    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
-        List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
-        return scopes.stream()
-                .map(SimpleGrantedAuthority::new)
-                .collect(Collectors.toList());
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-class CustomAuthoritiesOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
-    private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
-    override fun introspect(token: String): Mono<OAuth2AuthenticatedPrincipal> {
-        return delegate.introspect(token)
-                .map { principal: OAuth2AuthenticatedPrincipal ->
-                    DefaultOAuth2AuthenticatedPrincipal(
-                            principal.name, principal.attributes, extractAuthorities(principal))
-                }
-    }
-
-    private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection<GrantedAuthority> {
-        val scopes = principal.getAttribute<List<String>>(OAuth2IntrospectionClaimNames.SCOPE)
-        return scopes
-                .map { SimpleGrantedAuthority(it) }
-    }
-}
-----
-====
-
-Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public ReactiveOpaqueTokenIntrospector introspector() {
-    return new CustomAuthoritiesOpaqueTokenIntrospector();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun introspector(): ReactiveOpaqueTokenIntrospector {
-    return CustomAuthoritiesOpaqueTokenIntrospector()
-}
-----
-====
-
-[[webflux-oauth2resourceserver-opaque-jwt-introspector]]
-=== Using Introspection with JWTs
-
-A common question is whether or not introspection is compatible with JWTs.
-Spring Security's Opaque Token support has been designed to not care about the format of the token -- it will gladly pass any token to the introspection endpoint provided.
-
-So, let's say that you've got a requirement that requires you to check with the authorization server on each request, in case the JWT has been revoked.
-
-Even though you are using the JWT format for the token, your validation method is introspection, meaning you'd want to do:
-
-[source,yaml]
-----
-spring:
-  security:
-    oauth2:
-      resourceserver:
-        opaque-token:
-          introspection-uri: https://idp.example.org/introspection
-          client-id: client
-          client-secret: secret
-----
-
-In this case, the resulting `Authentication` would be `BearerTokenAuthentication`.
-Any attributes in the corresponding `OAuth2AuthenticatedPrincipal` would be whatever was returned by the introspection endpoint.
-
-But, let's say that, oddly enough, the introspection endpoint only returns whether or not the token is active.
-Now what?
-
-In this case, you can create a custom `ReactiveOpaqueTokenIntrospector` that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes:
-
-====
-.Java
-[source,java,role="primary"]
-----
-public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
-	private ReactiveOpaqueTokenIntrospector delegate =
-			new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
-	private ReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(new ParseOnlyJWTProcessor());
-
-	public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
-		return this.delegate.introspect(token)
-				.flatMap(principal -> this.jwtDecoder.decode(token))
-				.map(jwt -> new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES));
-	}
-
-	private static class ParseOnlyJWTProcessor implements Converter<JWT, Mono<JWTClaimsSet>> {
-		public Mono<JWTClaimsSet> convert(JWT jwt) {
-			try {
-				return Mono.just(jwt.getJWTClaimsSet());
-			} catch (Exception ex) {
-				return Mono.error(ex);
-			}
-		}
-	}
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-class JwtOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
-    private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
-    private val jwtDecoder: ReactiveJwtDecoder = NimbusReactiveJwtDecoder(ParseOnlyJWTProcessor())
-    override fun introspect(token: String): Mono<OAuth2AuthenticatedPrincipal> {
-        return delegate.introspect(token)
-                .flatMap { jwtDecoder.decode(token) }
-                .map { jwt: Jwt -> DefaultOAuth2AuthenticatedPrincipal(jwt.claims, NO_AUTHORITIES) }
-    }
-
-    private class ParseOnlyJWTProcessor : Converter<JWT, Mono<JWTClaimsSet>> {
-        override fun convert(jwt: JWT): Mono<JWTClaimsSet> {
-            return try {
-                Mono.just(jwt.jwtClaimsSet)
-            } catch (e: Exception) {
-                Mono.error(e)
-            }
-        }
-    }
-}
-----
-====
-
-Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public ReactiveOpaqueTokenIntrospector introspector() {
-    return new JwtOpaqueTokenIntropsector();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun introspector(): ReactiveOpaqueTokenIntrospector {
-    return JwtOpaqueTokenIntrospector()
-}
-----
-====
-
-[[webflux-oauth2resourceserver-opaque-userinfo]]
-=== Calling a `/userinfo` Endpoint
-
-Generally speaking, a Resource Server doesn't care about the underlying user, but instead about the authorities that have been granted.
-
-That said, at times it can be valuable to tie the authorization statement back to a user.
-
-If an application is also using `spring-security-oauth2-client`, having set up the appropriate `ClientRegistrationRepository`, then this is quite simple with a custom `OpaqueTokenIntrospector`.
-This implementation below does three things:
-
-* Delegates to the introspection endpoint, to affirm the token's validity
-* Looks up the appropriate client registration associated with the `/userinfo` endpoint
-* Invokes and returns the response from the `/userinfo` endpoint
-
-====
-.Java
-[source,java,role="primary"]
-----
-public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
-	private final ReactiveOpaqueTokenIntrospector delegate =
-			new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
-	private final ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService =
-			new DefaultReactiveOAuth2UserService();
-
-	private final ReactiveClientRegistrationRepository repository;
-
-	// ... constructor
-
-	@Override
-	public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
-		return Mono.zip(this.delegate.introspect(token), this.repository.findByRegistrationId("registration-id"))
-				.map(t -> {
-					OAuth2AuthenticatedPrincipal authorized = t.getT1();
-					ClientRegistration clientRegistration = t.getT2();
-					Instant issuedAt = authorized.getAttribute(ISSUED_AT);
-					Instant expiresAt = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT);
-					OAuth2AccessToken accessToken = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
-					return new OAuth2UserRequest(clientRegistration, accessToken);
-				})
-				.flatMap(this.oauth2UserService::loadUser);
-	}
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
-    private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
-    private val oauth2UserService: ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> = DefaultReactiveOAuth2UserService()
-    private val repository: ReactiveClientRegistrationRepository? = null
-
-    // ... constructor
-    override fun introspect(token: String?): Mono<OAuth2AuthenticatedPrincipal> {
-        return Mono.zip<OAuth2AuthenticatedPrincipal, ClientRegistration>(delegate.introspect(token), repository!!.findByRegistrationId("registration-id"))
-                .map<OAuth2UserRequest> { t: Tuple2<OAuth2AuthenticatedPrincipal, ClientRegistration> ->
-                    val authorized = t.t1
-                    val clientRegistration = t.t2
-                    val issuedAt: Instant? = authorized.getAttribute(ISSUED_AT)
-                    val expiresAt: Instant? = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT)
-                    val accessToken = OAuth2AccessToken(BEARER, token, issuedAt, expiresAt)
-                    OAuth2UserRequest(clientRegistration, accessToken)
-                }
-                .flatMap { userRequest: OAuth2UserRequest -> oauth2UserService.loadUser(userRequest) }
-    }
-}
-----
-====
-
-If you aren't using `spring-security-oauth2-client`, it's still quite simple.
-You will simply need to invoke the `/userinfo` with your own instance of `WebClient`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
-    private final ReactiveOpaqueTokenIntrospector delegate =
-            new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
-    private final WebClient rest = WebClient.create();
-
-    @Override
-    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
-        return this.delegate.introspect(token)
-		        .map(this::makeUserInfoRequest);
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
-    private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
-    private val rest: WebClient = WebClient.create()
-
-    override fun introspect(token: String): Mono<OAuth2AuthenticatedPrincipal> {
-        return delegate.introspect(token)
-                .map(this::makeUserInfoRequest)
-    }
-}
-----
-====
-
-Either way, having created your `ReactiveOpaqueTokenIntrospector`, you should publish it as a `@Bean` to override the defaults:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-ReactiveOpaqueTokenIntrospector introspector() {
-    return new UserInfoOpaqueTokenIntrospector();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun introspector(): ReactiveOpaqueTokenIntrospector {
-    return UserInfoOpaqueTokenIntrospector()
-}
-----
-====
-
-[[webflux-oauth2resourceserver-multitenancy]]
-== Multi-tenancy
-
-A resource server is considered multi-tenant when there are multiple strategies for verifying a bearer token, keyed by some tenant identifier.
-
-For example, your resource server may accept bearer tokens from two different authorization servers.
-Or, your authorization server may represent a multiplicity of issuers.
-
-In each case, there are two things that need to be done and trade-offs associated with how you choose to do them:
-
-1. Resolve the tenant
-2. Propagate the tenant
-
-=== Resolving the Tenant By Claim
-
-One way to differentiate tenants is by the issuer claim. Since the issuer claim accompanies signed JWTs, this can be done with the `JwtIssuerReactiveAuthenticationManagerResolver`, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver
-    ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
-
-http
-    .authorizeExchange(exchanges -> exchanges
-        .anyExchange().authenticated()
-    )
-    .oauth2ResourceServer(oauth2 -> oauth2
-        .authenticationManagerResolver(authenticationManagerResolver)
-    );
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo")
-
-return http {
-    authorizeExchange {
-        authorize(anyExchange, authenticated)
-    }
-    oauth2ResourceServer {
-        authenticationManagerResolver = customAuthenticationManagerResolver
-    }
-}
-----
-====
-
-This is nice because the issuer endpoints are loaded lazily.
-In fact, the corresponding `JwtReactiveAuthenticationManager` is instantiated only when the first request with the corresponding issuer is sent.
-This allows for an application startup that is independent from those authorization servers being up and available.
-
-==== Dynamic Tenants
-
-Of course, you may not want to restart the application each time a new tenant is added.
-In this case, you can configure the `JwtIssuerReactiveAuthenticationManagerResolver` with a repository of `ReactiveAuthenticationManager` instances, which you can edit at runtime, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-private Mono<ReactiveAuthenticationManager> addManager(
-		Map<String, ReactiveAuthenticationManager> authenticationManagers, String issuer) {
-
-	return Mono.fromCallable(() -> ReactiveJwtDecoders.fromIssuerLocation(issuer))
-            .subscribeOn(Schedulers.boundedElastic())
-            .map(JwtReactiveAuthenticationManager::new)
-            .doOnNext(authenticationManager -> authenticationManagers.put(issuer, authenticationManager));
-}
-
-// ...
-
-JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver =
-        new JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get);
-
-http
-    .authorizeExchange(exchanges -> exchanges
-        .anyExchange().authenticated()
-    )
-    .oauth2ResourceServer(oauth2 -> oauth2
-        .authenticationManagerResolver(authenticationManagerResolver)
-    );
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-private fun addManager(
-        authenticationManagers: MutableMap<String, ReactiveAuthenticationManager>, issuer: String): Mono<JwtReactiveAuthenticationManager> {
-    return Mono.fromCallable { ReactiveJwtDecoders.fromIssuerLocation(issuer) }
-            .subscribeOn(Schedulers.boundedElastic())
-            .map { jwtDecoder: ReactiveJwtDecoder -> JwtReactiveAuthenticationManager(jwtDecoder) }
-            .doOnNext { authenticationManager: JwtReactiveAuthenticationManager -> authenticationManagers[issuer] = authenticationManager }
-}
-
-// ...
-
-var customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get)
-return http {
-    authorizeExchange {
-        authorize(anyExchange, authenticated)
-    }
-    oauth2ResourceServer {
-        authenticationManagerResolver = customAuthenticationManagerResolver
-    }
-}
-----
-====
-
-In this case, you construct `JwtIssuerReactiveAuthenticationManagerResolver` with a strategy for obtaining the `ReactiveAuthenticationManager` given the issuer.
-This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime.
-
-NOTE: It would be unsafe to simply take any issuer and construct an `ReactiveAuthenticationManager` from it.
-The issuer should be one that the code can verify from a trusted source like an allowed list of issuers.
-
-[[webflux-oauth2resourceserver-bearertoken-resolver]]
-== Bearer Token Resolution
-
-By default, Resource Server looks for a bearer token in the `Authorization` header.
-This, however, can be customized.
-
-For example, you may have a need to read the bearer token from a custom header.
-To achieve this, you can wire an instance of `ServerBearerTokenAuthenticationConverter` into the DSL, as you can see in the following example:
-
-.Custom Bearer Token Header
-====
-.Java
-[source,java,role="primary"]
-----
-ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter();
-converter.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION);
-http
-    .oauth2ResourceServer(oauth2 -> oauth2
-        .bearerTokenConverter(converter)
-    );
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val converter = ServerBearerTokenAuthenticationConverter()
-converter.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION)
-return http {
-    oauth2ResourceServer {
-        bearerTokenConverter = converter
-    }
-}
-----
-====
-
-== Bearer Token Propagation
-
-Now that you're in possession of a bearer token, it might be handy to pass that to downstream services.
-This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.html[ServerBearerExchangeFilterFunction]`, which you can see in the following example:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-public WebClient rest() {
-    return WebClient.builder()
-            .filter(new ServerBearerExchangeFilterFunction())
-            .build();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun rest(): WebClient {
-    return WebClient.builder()
-            .filter(ServerBearerExchangeFilterFunction())
-            .build()
-}
-----
-====
-
-When the above `WebClient` is used to perform requests, Spring Security will look up the current `Authentication` and extract any `{security-api-url}org/springframework/security/oauth2/core/AbstractOAuth2Token.html[AbstractOAuth2Token]` credential.
-Then, it will propagate that token in the `Authorization` header.
-
-For example:
-
-====
-.Java
-[source,java,role="primary"]
-----
-this.rest.get()
-        .uri("https://other-service.example.com/endpoint")
-        .retrieve()
-        .bodyToMono(String.class)
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-this.rest.get()
-        .uri("https://other-service.example.com/endpoint")
-        .retrieve()
-        .bodyToMono<String>()
-----
-====
-
-Will invoke the `https://other-service.example.com/endpoint`, adding the bearer token `Authorization` header for you.
-
-In places where you need to override this behavior, it's a simple matter of supplying the header yourself, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-this.rest.get()
-        .uri("https://other-service.example.com/endpoint")
-        .headers(headers -> headers.setBearerAuth(overridingToken))
-        .retrieve()
-        .bodyToMono(String.class)
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-rest.get()
-        .uri("https://other-service.example.com/endpoint")
-        .headers { it.setBearerAuth(overridingToken) }
-        .retrieve()
-        .bodyToMono<String>()
-----
-====
-
-In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain.
-
-[NOTE]
-Unlike the https://docs.spring.io/spring-security/site/docs/current-SNAPSHOT/api/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.html[OAuth 2.0 Client filter function], this filter function makes no attempt to renew the token, should it be expired.
-To obtain this level of support, please use the OAuth 2.0 Client filter.

+ 122 - 0
docs/modules/ROOT/pages/reactive/oauth2/resource-server/bearer-tokens.adoc

@@ -0,0 +1,122 @@
+= OAuth 2.0 Resource Server Bearer Tokens
+
+[[webflux-oauth2resourceserver-bearertoken-resolver]]
+== Bearer Token Resolution
+
+By default, Resource Server looks for a bearer token in the `Authorization` header.
+This, however, can be customized.
+
+For example, you may have a need to read the bearer token from a custom header.
+To achieve this, you can wire an instance of `ServerBearerTokenAuthenticationConverter` into the DSL, as you can see in the following example:
+
+.Custom Bearer Token Header
+====
+.Java
+[source,java,role="primary"]
+----
+ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter();
+converter.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION);
+http
+    .oauth2ResourceServer(oauth2 -> oauth2
+        .bearerTokenConverter(converter)
+    );
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+val converter = ServerBearerTokenAuthenticationConverter()
+converter.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION)
+return http {
+    oauth2ResourceServer {
+        bearerTokenConverter = converter
+    }
+}
+----
+====
+
+== Bearer Token Propagation
+
+Now that you're in possession of a bearer token, it might be handy to pass that to downstream services.
+This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.html[ServerBearerExchangeFilterFunction]`, which you can see in the following example:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public WebClient rest() {
+    return WebClient.builder()
+            .filter(new ServerBearerExchangeFilterFunction())
+            .build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun rest(): WebClient {
+    return WebClient.builder()
+            .filter(ServerBearerExchangeFilterFunction())
+            .build()
+}
+----
+====
+
+When the above `WebClient` is used to perform requests, Spring Security will look up the current `Authentication` and extract any `{security-api-url}org/springframework/security/oauth2/core/AbstractOAuth2Token.html[AbstractOAuth2Token]` credential.
+Then, it will propagate that token in the `Authorization` header.
+
+For example:
+
+====
+.Java
+[source,java,role="primary"]
+----
+this.rest.get()
+        .uri("https://other-service.example.com/endpoint")
+        .retrieve()
+        .bodyToMono(String.class)
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+this.rest.get()
+        .uri("https://other-service.example.com/endpoint")
+        .retrieve()
+        .bodyToMono<String>()
+----
+====
+
+Will invoke the `https://other-service.example.com/endpoint`, adding the bearer token `Authorization` header for you.
+
+In places where you need to override this behavior, it's a simple matter of supplying the header yourself, like so:
+
+====
+.Java
+[source,java,role="primary"]
+----
+this.rest.get()
+        .uri("https://other-service.example.com/endpoint")
+        .headers(headers -> headers.setBearerAuth(overridingToken))
+        .retrieve()
+        .bodyToMono(String.class)
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+rest.get()
+        .uri("https://other-service.example.com/endpoint")
+        .headers { it.setBearerAuth(overridingToken) }
+        .retrieve()
+        .bodyToMono<String>()
+----
+====
+
+In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain.
+
+[NOTE]
+Unlike the https://docs.spring.io/spring-security/site/docs/current-SNAPSHOT/api/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.html[OAuth 2.0 Client filter function], this filter function makes no attempt to renew the token, should it be expired.
+To obtain this level of support, please use the OAuth 2.0 Client filter.

+ 15 - 0
docs/modules/ROOT/pages/reactive/oauth2/resource-server/index.adoc

@@ -0,0 +1,15 @@
+[[webflux-oauth2-resource-server]]
+= OAuth 2.0 Resource Server
+
+Spring Security supports protecting endpoints using two forms of OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]:
+
+* https://tools.ietf.org/html/rfc7519[JWT]
+* Opaque Tokens
+
+This is handy in circumstances where an application has delegated its authority management to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity).
+This authorization server can be consulted by resource servers to authorize requests.
+
+[NOTE]
+====
+A complete working example for {gh-samples-url}/reactive/webflux/java/oauth2/resource-server[*JWTs*]  is available in the {gh-samples-url}[Spring Security repository].
+====

+ 882 - 0
docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc

@@ -0,0 +1,882 @@
+= OAuth 2.0 Resource Server JWT
+
+[[webflux-oauth2resourceserver-jwt-minimaldependencies]]
+== Minimal Dependencies for JWT
+
+Most Resource Server support is collected into `spring-security-oauth2-resource-server`.
+However, the support for decoding and verifying JWTs is in `spring-security-oauth2-jose`, meaning that both are necessary in order to have a working resource server that supports JWT-encoded Bearer Tokens.
+
+[[webflux-oauth2resourceserver-jwt-minimalconfiguration]]
+== Minimal Configuration for JWTs
+
+When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a resource server consists of two basic steps.
+First, include the needed dependencies and second, indicate the location of the authorization server.
+
+=== Specifying the Authorization Server
+
+In a Spring Boot application, to specify which authorization server to use, simply do:
+
+[source,yml]
+----
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          issuer-uri: https://idp.example.com/issuer
+----
+
+Where `https://idp.example.com/issuer` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
+Resource Server will use this property to further self-configure, discover the authorization server's public keys, and subsequently validate incoming JWTs.
+
+[NOTE]
+To use the `issuer-uri` property, it must also be true that one of `https://idp.example.com/issuer/.well-known/openid-configuration`, `https://idp.example.com/.well-known/openid-configuration/issuer`, or `https://idp.example.com/.well-known/oauth-authorization-server/issuer` is a supported endpoint for the authorization server.
+This endpoint is referred to as a https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Provider Configuration] endpoint or a https://tools.ietf.org/html/rfc8414#section-3[Authorization Server Metadata] endpoint.
+
+And that's it!
+
+=== Startup Expectations
+
+When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
+
+It achieves this through a deterministic startup process:
+
+1. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property
+2. Configure the validation strategy to query `jwks_url` for valid public keys
+3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
+
+A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
+
+[NOTE]
+If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.
+
+=== Runtime Expectations
+
+Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
+
+[source,html]
+----
+GET / HTTP/1.1
+Authorization: Bearer some-token-value # Resource Server will process this
+----
+
+So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
+
+Given a well-formed JWT, Resource Server will:
+
+1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
+2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
+3. Map each scope to an authority with the prefix `SCOPE_`.
+
+[NOTE]
+As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
+
+The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
+
+From here, consider jumping to:
+
+<<webflux-oauth2resourceserver-jwt-jwkseturi,How to Configure without Tying Resource Server startup to an authorization server's availability>>
+
+<<webflux-oauth2resourceserver-jwt-sansboot,How to Configure without Spring Boot>>
+
+[[webflux-oauth2resourceserver-jwt-jwkseturi]]
+=== Specifying the Authorization Server JWK Set Uri Directly
+
+If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
+
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          issuer-uri: https://idp.example.com
+          jwk-set-uri: https://idp.example.com/.well-known/jwks.json
+----
+
+[NOTE]
+The JWK Set uri is not standardized, but can typically be found in the authorization server's documentation
+
+Consequently, Resource Server will not ping the authorization server at startup.
+We still specify the `issuer-uri` so that Resource Server still validates the `iss` claim on incoming JWTs.
+
+[NOTE]
+This property can also be supplied directly on the <<webflux-oauth2resourceserver-jwt-jwkseturi-dsl,DSL>>.
+
+[[webflux-oauth2resourceserver-jwt-sansboot]]
+=== Overriding or Replacing Boot Auto Configuration
+
+There are two ``@Bean``s that Spring Boot generates on Resource Server's behalf.
+
+The first is a `SecurityWebFilterChain` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `SecurityWebFilterChain` looks like:
+
+.Resource Server SecurityWebFilterChain
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	http
+		.authorizeExchange(exchanges -> exchanges
+			.anyExchange().authenticated()
+		)
+		.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
+	return http.build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            jwt { }
+        }
+    }
+}
+----
+====
+
+If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one.
+
+Replacing this is as simple as exposing the bean within the application:
+
+.Replacing SecurityWebFilterChain
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	http
+		.authorizeExchange(exchanges -> exchanges
+			.pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
+			.anyExchange().authenticated()
+		)
+		.oauth2ResourceServer(oauth2 -> oauth2
+			.jwt(withDefaults())
+		);
+	return http.build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize("/message/**", hasAuthority("SCOPE_message:read"))
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            jwt { }
+        }
+    }
+}
+----
+====
+
+The above requires the scope of `message:read` for any URL that starts with `/messages/`.
+
+Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration.
+
+For example, the second `@Bean` Spring Boot creates is a `ReactiveJwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`:
+
+.ReactiveJwtDecoder
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveJwtDecoder jwtDecoder() {
+    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
+}
+----
+====
+
+[NOTE]
+Calling `{security-api-url}org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-[ReactiveJwtDecoders#fromIssuerLocation]` is what invokes the Provider Configuration or Authorization Server Metadata endpoint in order to derive the JWK Set Uri.
+If the application doesn't expose a `ReactiveJwtDecoder` bean, then Spring Boot will expose the above default one.
+
+And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`.
+
+[[webflux-oauth2resourceserver-jwt-jwkseturi-dsl]]
+==== Using `jwkSetUri()`
+
+An authorization server's JWK Set Uri can be configured <<webflux-oauth2resourceserver-jwt-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	http
+		.authorizeExchange(exchanges -> exchanges
+			.anyExchange().authenticated()
+		)
+		.oauth2ResourceServer(oauth2 -> oauth2
+			.jwt(jwt -> jwt
+				.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
+			)
+		);
+	return http.build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            jwt {
+                jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
+            }
+        }
+    }
+}
+----
+====
+
+Using `jwkSetUri()` takes precedence over any configuration property.
+
+[[webflux-oauth2resourceserver-jwt-decoder-dsl]]
+==== Using `decoder()`
+
+More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	http
+		.authorizeExchange(exchanges -> exchanges
+			.anyExchange().authenticated()
+		)
+		.oauth2ResourceServer(oauth2 -> oauth2
+			.jwt(jwt -> jwt
+				.decoder(myCustomDecoder())
+			)
+		);
+    return http.build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            jwt {
+                jwtDecoder = myCustomDecoder()
+            }
+        }
+    }
+}
+----
+====
+
+This is handy when deeper configuration, like <<webflux-oauth2resourceserver-jwt-validation,validation>>, is necessary.
+
+[[webflux-oauth2resourceserver-decoder-bean]]
+==== Exposing a `ReactiveJwtDecoder` `@Bean`
+
+Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveJwtDecoder jwtDecoder() {
+    return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
+}
+----
+====
+
+[[webflux-oauth2resourceserver-jwt-decoder-algorithm]]
+== Configuring Trusted Algorithms
+
+By default, `NimbusReactiveJwtDecoder`, and hence Resource Server, will only trust and verify tokens using `RS256`.
+
+You can customize this via <<webflux-oauth2resourceserver-jwt-boot-algorithm,Spring Boot>> or <<webflux-oauth2resourceserver-jwt-decoder-builder,the NimbusJwtDecoder builder>>.
+
+[[webflux-oauth2resourceserver-jwt-boot-algorithm]]
+=== Via Spring Boot
+
+The simplest way to set the algorithm is as a property:
+
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          jws-algorithm: RS512
+          jwk-set-uri: https://idp.example.org/.well-known/jwks.json
+----
+
+[[webflux-oauth2resourceserver-jwt-decoder-builder]]
+=== Using a Builder
+
+For greater power, though, we can use a builder that ships with `NimbusReactiveJwtDecoder`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+ReactiveJwtDecoder jwtDecoder() {
+    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+            .jwsAlgorithm(RS512).build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+            .jwsAlgorithm(RS512).build()
+}
+----
+====
+
+Calling `jwsAlgorithm` more than once will configure `NimbusReactiveJwtDecoder` to trust more than one algorithm, like so:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+ReactiveJwtDecoder jwtDecoder() {
+    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
+}
+----
+====
+
+Or, you can call `jwsAlgorithms`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+ReactiveJwtDecoder jwtDecoder() {
+    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+            .jwsAlgorithms(algorithms -> {
+                    algorithms.add(RS512);
+                    algorithms.add(ES512);
+            }).build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
+            .jwsAlgorithms {
+                it.add(RS512)
+                it.add(ES512)
+            }
+            .build()
+}
+----
+====
+
+[[webflux-oauth2resourceserver-jwt-decoder-public-key]]
+=== Trusting a Single Asymmetric Key
+
+Simpler than backing a Resource Server with a JWK Set endpoint is to hard-code an RSA public key.
+The public key can be provided via <<webflux-oauth2resourceserver-jwt-decoder-public-key-boot,Spring Boot>> or by <<webflux-oauth2resourceserver-jwt-decoder-public-key-builder,Using a Builder>>.
+
+[[webflux-oauth2resourceserver-jwt-decoder-public-key-boot]]
+==== Via Spring Boot
+
+Specifying a key via Spring Boot is quite simple.
+The key's location can be specified like so:
+
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          public-key-location: classpath:my-key.pub
+----
+
+Or, to allow for a more sophisticated lookup, you can post-process the `RsaKeyConversionServicePostProcessor`:
+
+.BeanFactoryPostProcessor
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+BeanFactoryPostProcessor conversionServiceCustomizer() {
+    return beanFactory ->
+        beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
+                .setResourceLoader(new CustomResourceLoader());
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
+    return BeanFactoryPostProcessor { beanFactory: ConfigurableListableBeanFactory ->
+        beanFactory.getBean<RsaKeyConversionServicePostProcessor>()
+                .setResourceLoader(CustomResourceLoader())
+    }
+}
+----
+====
+
+Specify your key's location:
+
+[source,yaml]
+----
+key.location: hfds://my-key.pub
+----
+
+And then autowire the value:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Value("${key.location}")
+RSAPublicKey key;
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Value("\${key.location}")
+val key: RSAPublicKey? = null
+----
+====
+
+[[webflux-oauth2resourceserver-jwt-decoder-public-key-builder]]
+==== Using a Builder
+
+To wire an `RSAPublicKey` directly, you can simply use the appropriate `NimbusReactiveJwtDecoder` builder, like so:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveJwtDecoder jwtDecoder() {
+    return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return NimbusReactiveJwtDecoder.withPublicKey(key).build()
+}
+----
+====
+
+[[webflux-oauth2resourceserver-jwt-decoder-secret-key]]
+=== Trusting a Single Symmetric Key
+
+Using a single symmetric key is also simple.
+You can simply load in your `SecretKey` and use the appropriate `NimbusReactiveJwtDecoder` builder, like so:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveJwtDecoder jwtDecoder() {
+    return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    return NimbusReactiveJwtDecoder.withSecretKey(this.key).build()
+}
+----
+====
+
+[[webflux-oauth2resourceserver-jwt-authorization]]
+=== Configuring Authorization
+
+A JWT that is issued from an OAuth 2.0 Authorization Server will typically either have a `scope` or `scp` attribute, indicating the scopes (or authorities) it's been granted, for example:
+
+`{ ..., "scope" : "messages contacts"}`
+
+When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
+
+This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	http
+		.authorizeExchange(exchanges -> exchanges
+			.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
+			.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
+			.anyExchange().authenticated()
+		)
+		.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
+    return http.build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
+            authorize("/messages/**", hasAuthority("SCOPE_messages"))
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            jwt { }
+        }
+    }
+}
+----
+====
+
+Or similarly with method security:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@PreAuthorize("hasAuthority('SCOPE_messages')")
+public Flux<Message> getMessages(...) {}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@PreAuthorize("hasAuthority('SCOPE_messages')")
+fun getMessages(): Flux<Message> { }
+----
+====
+
+[[webflux-oauth2resourceserver-jwt-authorization-extraction]]
+==== Extracting Authorities Manually
+
+However, there are a number of circumstances where this default is insufficient.
+For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute.
+Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities.
+
+To this end, the DSL exposes `jwtAuthenticationConverter()`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	http
+		.authorizeExchange(exchanges -> exchanges
+			.anyExchange().authenticated()
+		)
+		.oauth2ResourceServer(oauth2 -> oauth2
+			.jwt(jwt -> jwt
+				.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
+			)
+		);
+	return http.build();
+}
+
+Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
+    JwtAuthenticationConverter jwtAuthenticationConverter =
+            new JwtAuthenticationConverter();
+    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
+            (new GrantedAuthoritiesExtractor());
+    return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            jwt {
+                jwtAuthenticationConverter = grantedAuthoritiesExtractor()
+            }
+        }
+    }
+}
+
+fun grantedAuthoritiesExtractor(): Converter<Jwt, Mono<AbstractAuthenticationToken>> {
+    val jwtAuthenticationConverter = JwtAuthenticationConverter()
+    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor())
+    return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter)
+}
+----
+====
+
+which is responsible for converting a `Jwt` into an `Authentication`.
+As part of its configuration, we can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities.
+
+That final converter might be something like `GrantedAuthoritiesExtractor` below:
+
+====
+.Java
+[source,java,role="primary"]
+----
+static class GrantedAuthoritiesExtractor
+        implements Converter<Jwt, Collection<GrantedAuthority>> {
+
+    public Collection<GrantedAuthority> convert(Jwt jwt) {
+        Collection<?> authorities = (Collection<?>)
+                jwt.getClaims().getOrDefault("mycustomclaim", Collections.emptyList());
+
+        return authorities.stream()
+                .map(Object::toString)
+                .map(SimpleGrantedAuthority::new)
+                .collect(Collectors.toList());
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+internal class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAuthority>> {
+    override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
+        val authorities: List<Any> = jwt.claims
+                .getOrDefault("mycustomclaim", emptyList<Any>()) as List<Any>
+        return authorities
+                .map { it.toString() }
+                .map { SimpleGrantedAuthority(it) }
+    }
+}
+----
+====
+
+For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter<Jwt, Mono<AbstractAuthenticationToken>>`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
+    public AbstractAuthenticationToken convert(Jwt jwt) {
+        return Mono.just(jwt).map(this::doConversion);
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
+    override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
+        return Mono.just(jwt).map(this::doConversion)
+    }
+}
+----
+====
+
+[[webflux-oauth2resourceserver-jwt-validation]]
+=== Configuring Validation
+
+Using <<webflux-oauth2resourceserver-jwt-minimalconfiguration,minimal Spring Boot configuration>>, indicating the authorization server's issuer uri, Resource Server will default to verifying the `iss` claim as well as the `exp` and `nbf` timestamp claims.
+
+In circumstances where validation needs to be customized, Resource Server ships with two standard validators and also accepts custom `OAuth2TokenValidator` instances.
+
+[[webflux-oauth2resourceserver-jwt-validation-clockskew]]
+==== Customizing Timestamp Validation
+
+JWT's typically have a window of validity, with the start of the window indicated in the `nbf` claim and the end indicated in the `exp` claim.
+
+However, every server can experience clock drift, which can cause tokens to appear expired to one server, but not to another.
+This can cause some implementation heartburn as the number of collaborating servers increases in a distributed system.
+
+Resource Server uses `JwtTimestampValidator` to verify a token's validity window, and it can be configured with a `clockSkew` to alleviate the above problem:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+ReactiveJwtDecoder jwtDecoder() {
+     NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
+             ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
+
+     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
+            new JwtTimestampValidator(Duration.ofSeconds(60)),
+            new IssuerValidator(issuerUri));
+
+     jwtDecoder.setJwtValidator(withClockSkew);
+
+     return jwtDecoder;
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
+    val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
+            JwtTimestampValidator(Duration.ofSeconds(60)),
+            JwtIssuerValidator(issuerUri))
+    jwtDecoder.setJwtValidator(withClockSkew)
+    return jwtDecoder
+}
+----
+====
+
+[NOTE]
+By default, Resource Server configures a clock skew of 60 seconds.
+
+[[webflux-oauth2resourceserver-validation-custom]]
+==== Configuring a Custom Validator
+
+Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API:
+
+====
+.Java
+[source,java,role="primary"]
+----
+public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
+    OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
+
+    public OAuth2TokenValidatorResult validate(Jwt jwt) {
+        if (jwt.getAudience().contains("messaging")) {
+            return OAuth2TokenValidatorResult.success();
+        } else {
+            return OAuth2TokenValidatorResult.failure(error);
+        }
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+class AudienceValidator : OAuth2TokenValidator<Jwt> {
+    var error: OAuth2Error = OAuth2Error("invalid_token", "The required audience is missing", null)
+    override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
+        return if (jwt.audience.contains("messaging")) {
+            OAuth2TokenValidatorResult.success()
+        } else {
+            OAuth2TokenValidatorResult.failure(error)
+        }
+    }
+}
+----
+====
+
+Then, to add into a resource server, it's a matter of specifying the `ReactiveJwtDecoder` instance:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+ReactiveJwtDecoder jwtDecoder() {
+    NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
+            ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
+
+    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
+    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
+    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
+
+    jwtDecoder.setJwtValidator(withAudience);
+
+    return jwtDecoder;
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun jwtDecoder(): ReactiveJwtDecoder {
+    val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
+    val audienceValidator: OAuth2TokenValidator<Jwt> = AudienceValidator()
+    val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
+    val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)
+    jwtDecoder.setJwtValidator(withAudience)
+    return jwtDecoder
+}
+----
+====

+ 117 - 0
docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc

@@ -0,0 +1,117 @@
+= OAuth 2.0 Resource Server Multitenancy
+
+[[webflux-oauth2resourceserver-multitenancy]]
+== Multi-tenancy
+
+A resource server is considered multi-tenant when there are multiple strategies for verifying a bearer token, keyed by some tenant identifier.
+
+For example, your resource server may accept bearer tokens from two different authorization servers.
+Or, your authorization server may represent a multiplicity of issuers.
+
+In each case, there are two things that need to be done and trade-offs associated with how you choose to do them:
+
+1. Resolve the tenant
+2. Propagate the tenant
+
+=== Resolving the Tenant By Claim
+
+One way to differentiate tenants is by the issuer claim. Since the issuer claim accompanies signed JWTs, this can be done with the `JwtIssuerReactiveAuthenticationManagerResolver`, like so:
+
+====
+.Java
+[source,java,role="primary"]
+----
+JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver
+    ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
+
+http
+    .authorizeExchange(exchanges -> exchanges
+        .anyExchange().authenticated()
+    )
+    .oauth2ResourceServer(oauth2 -> oauth2
+        .authenticationManagerResolver(authenticationManagerResolver)
+    );
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+val customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo")
+
+return http {
+    authorizeExchange {
+        authorize(anyExchange, authenticated)
+    }
+    oauth2ResourceServer {
+        authenticationManagerResolver = customAuthenticationManagerResolver
+    }
+}
+----
+====
+
+This is nice because the issuer endpoints are loaded lazily.
+In fact, the corresponding `JwtReactiveAuthenticationManager` is instantiated only when the first request with the corresponding issuer is sent.
+This allows for an application startup that is independent from those authorization servers being up and available.
+
+==== Dynamic Tenants
+
+Of course, you may not want to restart the application each time a new tenant is added.
+In this case, you can configure the `JwtIssuerReactiveAuthenticationManagerResolver` with a repository of `ReactiveAuthenticationManager` instances, which you can edit at runtime, like so:
+
+====
+.Java
+[source,java,role="primary"]
+----
+private Mono<ReactiveAuthenticationManager> addManager(
+		Map<String, ReactiveAuthenticationManager> authenticationManagers, String issuer) {
+
+	return Mono.fromCallable(() -> ReactiveJwtDecoders.fromIssuerLocation(issuer))
+            .subscribeOn(Schedulers.boundedElastic())
+            .map(JwtReactiveAuthenticationManager::new)
+            .doOnNext(authenticationManager -> authenticationManagers.put(issuer, authenticationManager));
+}
+
+// ...
+
+JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver =
+        new JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get);
+
+http
+    .authorizeExchange(exchanges -> exchanges
+        .anyExchange().authenticated()
+    )
+    .oauth2ResourceServer(oauth2 -> oauth2
+        .authenticationManagerResolver(authenticationManagerResolver)
+    );
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+private fun addManager(
+        authenticationManagers: MutableMap<String, ReactiveAuthenticationManager>, issuer: String): Mono<JwtReactiveAuthenticationManager> {
+    return Mono.fromCallable { ReactiveJwtDecoders.fromIssuerLocation(issuer) }
+            .subscribeOn(Schedulers.boundedElastic())
+            .map { jwtDecoder: ReactiveJwtDecoder -> JwtReactiveAuthenticationManager(jwtDecoder) }
+            .doOnNext { authenticationManager: JwtReactiveAuthenticationManager -> authenticationManagers[issuer] = authenticationManager }
+}
+
+// ...
+
+var customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get)
+return http {
+    authorizeExchange {
+        authorize(anyExchange, authenticated)
+    }
+    oauth2ResourceServer {
+        authenticationManagerResolver = customAuthenticationManagerResolver
+    }
+}
+----
+====
+
+In this case, you construct `JwtIssuerReactiveAuthenticationManagerResolver` with a strategy for obtaining the `ReactiveAuthenticationManager` given the issuer.
+This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime.
+
+NOTE: It would be unsafe to simply take any issuer and construct an `ReactiveAuthenticationManager` from it.
+The issuer should be one that the code can verify from a trusted source like an allowed list of issuers.

+ 775 - 0
docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc

@@ -0,0 +1,775 @@
+= OAuth 2.0 Resource Server Opaque Token
+
+[[webflux-oauth2resourceserver-opaque-minimaldependencies]]
+== Minimal Dependencies for Introspection
+As described in xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-minimaldependencies[Minimal Dependencies for JWT] most of Resource Server support is collected in `spring-security-oauth2-resource-server`.
+However unless a custom <<webflux-oauth2resourceserver-opaque-introspector-bean,`ReactiveOpaqueTokenIntrospector`>> is provided, the Resource Server will fallback to ReactiveOpaqueTokenIntrospector.
+Meaning that both `spring-security-oauth2-resource-server` and `oauth2-oidc-sdk` are necessary in order to have a working minimal Resource Server that supports opaque Bearer Tokens.
+Please refer to `spring-security-oauth2-resource-server` in order to determin the correct version for `oauth2-oidc-sdk`.
+
+[[webflux-oauth2resourceserver-opaque-minimalconfiguration]]
+== Minimal Configuration for Introspection
+
+Typically, an opaque token can be verified via an https://tools.ietf.org/html/rfc7662[OAuth 2.0 Introspection Endpoint], hosted by the authorization server.
+This can be handy when revocation is a requirement.
+
+When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a resource server that uses introspection consists of two basic steps.
+First, include the needed dependencies and second, indicate the introspection endpoint details.
+
+[[webflux-oauth2resourceserver-opaque-introspectionuri]]
+=== Specifying the Authorization Server
+
+To specify where the introspection endpoint is, simply do:
+
+[source,yaml]
+----
+security:
+  oauth2:
+    resourceserver:
+      opaque-token:
+        introspection-uri: https://idp.example.com/introspect
+        client-id: client
+        client-secret: secret
+----
+
+Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint.
+
+Resource Server will use these properties to further self-configure and subsequently validate incoming JWTs.
+
+[NOTE]
+When using introspection, the authorization server's word is the law.
+If the authorization server responses that the token is valid, then it is.
+
+And that's it!
+
+=== Startup Expectations
+
+When this property and these dependencies are used, Resource Server will automatically configure itself to validate Opaque Bearer Tokens.
+
+This startup process is quite a bit simpler than for JWTs since no endpoints need to be discovered and no additional validation rules get added.
+
+=== Runtime Expectations
+
+Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
+
+[source,http]
+----
+GET / HTTP/1.1
+Authorization: Bearer some-token-value # Resource Server will process this
+----
+
+So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
+
+Given an Opaque Token, Resource Server will
+
+1. Query the provided introspection endpoint using the provided credentials and the token
+2. Inspect the response for an `{ 'active' : true }` attribute
+3. Map each scope to an authority with the prefix `SCOPE_`
+
+The resulting `Authentication#getPrincipal`, by default, is a Spring Security `{security-api-url}org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.html[OAuth2AuthenticatedPrincipal]` object, and `Authentication#getName` maps to the token's `sub` property, if one is present.
+
+From here, you may want to jump to:
+
+* <<webflux-oauth2resourceserver-opaque-attributes,Looking Up Attributes Post-Authentication>>
+* <<webflux-oauth2resourceserver-opaque-authorization-extraction,Extracting Authorities Manually>>
+* <<webflux-oauth2resourceserver-opaque-jwt-introspector,Using Introspection with JWTs>>
+
+[[webflux-oauth2resourceserver-opaque-attributes]]
+== Looking Up Attributes Post-Authentication
+
+Once a token is authenticated, an instance of `BearerTokenAuthentication` is set in the `SecurityContext`.
+
+This means that it's available in `@Controller` methods when using `@EnableWebFlux` in your configuration:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@GetMapping("/foo")
+public Mono<String> foo(BearerTokenAuthentication authentication) {
+    return Mono.just(authentication.getTokenAttributes().get("sub") + " is the subject");
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@GetMapping("/foo")
+fun foo(authentication: BearerTokenAuthentication): Mono<String> {
+    return Mono.just(authentication.tokenAttributes["sub"].toString() + " is the subject")
+}
+----
+====
+
+Since `BearerTokenAuthentication` holds an `OAuth2AuthenticatedPrincipal`, that also means that it's available to controller methods, too:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@GetMapping("/foo")
+public Mono<String> foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
+    return Mono.just(principal.getAttribute("sub") + " is the subject");
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@GetMapping("/foo")
+fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): Mono<String> {
+    return Mono.just(principal.getAttribute<Any>("sub").toString() + " is the subject")
+}
+----
+====
+
+=== Looking Up Attributes Via SpEL
+
+Of course, this also means that attributes can be accessed via SpEL.
+
+For example, if using `@EnableReactiveMethodSecurity` so that you can use `@PreAuthorize` annotations, you can do:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@PreAuthorize("principal?.attributes['sub'] = 'foo'")
+public Mono<String> forFoosEyesOnly() {
+    return Mono.just("foo");
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@PreAuthorize("principal.attributes['sub'] = 'foo'")
+fun forFoosEyesOnly(): Mono<String> {
+    return Mono.just("foo")
+}
+----
+====
+
+[[webflux-oauth2resourceserver-opaque-sansboot]]
+== Overriding or Replacing Boot Auto Configuration
+
+There are two ``@Bean``s that Spring Boot generates on Resource Server's behalf.
+
+The first is a `SecurityWebFilterChain` that configures the app as a resource server.
+When use Opaque Token, this `SecurityWebFilterChain` looks like:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	http
+		.authorizeExchange(exchanges -> exchanges
+			.anyExchange().authenticated()
+		)
+		.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken)
+	return http.build();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            opaqueToken { }
+        }
+    }
+}
+----
+====
+
+If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one.
+
+Replacing this is as simple as exposing the bean within the application:
+
+.Replacing SecurityWebFilterChain
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableWebFluxSecurity
+public class MyCustomSecurityConfiguration {
+    @Bean
+    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+        http
+            .authorizeExchange(exchanges -> exchanges
+                .pathMatchers("/messages/**").hasAuthority("SCOPE_message:read")
+                .anyExchange().authenticated()
+            )
+            .oauth2ResourceServer(oauth2 -> oauth2
+                .opaqueToken(opaqueToken -> opaqueToken
+                    .introspector(myIntrospector())
+                )
+            );
+        return http.build();
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize("/messages/**", hasAuthority("SCOPE_message:read"))
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            opaqueToken {
+                introspector = myIntrospector()
+            }
+        }
+    }
+}
+----
+====
+
+The above requires the scope of `message:read` for any URL that starts with `/messages/`.
+
+Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration.
+
+For example, the second `@Bean` Spring Boot creates is a `ReactiveOpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveOpaqueTokenIntrospector introspector() {
+    return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun introspector(): ReactiveOpaqueTokenIntrospector {
+    return NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret)
+}
+----
+====
+
+If the application doesn't expose a `ReactiveOpaqueTokenIntrospector` bean, then Spring Boot will expose the above default one.
+
+And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`.
+
+[[webflux-oauth2resourceserver-opaque-introspectionuri-dsl]]
+=== Using `introspectionUri()`
+
+An authorization server's Introspection Uri can be configured <<webflux-oauth2resourceserver-opaque-introspectionuri,as a configuration property>> or it can be supplied in the DSL:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableWebFluxSecurity
+public class DirectlyConfiguredIntrospectionUri {
+    @Bean
+    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+        http
+            .authorizeExchange(exchanges -> exchanges
+                .anyExchange().authenticated()
+            )
+            .oauth2ResourceServer(oauth2 -> oauth2
+                .opaqueToken(opaqueToken -> opaqueToken
+                    .introspectionUri("https://idp.example.com/introspect")
+                    .introspectionClientCredentials("client", "secret")
+                )
+            );
+        return http.build();
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            opaqueToken {
+                introspectionUri = "https://idp.example.com/introspect"
+                introspectionClientCredentials("client", "secret")
+            }
+        }
+    }
+}
+----
+====
+
+Using `introspectionUri()` takes precedence over any configuration property.
+
+[[webflux-oauth2resourceserver-opaque-introspector-dsl]]
+=== Using `introspector()`
+
+More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of `ReactiveOpaqueTokenIntrospector`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableWebFluxSecurity
+public class DirectlyConfiguredIntrospector {
+    @Bean
+    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+        http
+            .authorizeExchange(exchanges -> exchanges
+                .anyExchange().authenticated()
+            )
+            .oauth2ResourceServer(oauth2 -> oauth2
+                .opaqueToken(opaqueToken -> opaqueToken
+                    .introspector(myCustomIntrospector())
+                )
+            );
+        return http.build();
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            opaqueToken {
+                introspector = myCustomIntrospector()
+            }
+        }
+    }
+}
+----
+====
+
+This is handy when deeper configuration, like <<webflux-oauth2resourceserver-opaque-authorization-extraction,authority mapping>>or <<webflux-oauth2resourceserver-opaque-jwt-introspector,JWT revocation>> is necessary.
+
+[[webflux-oauth2resourceserver-opaque-introspector-bean]]
+=== Exposing a `ReactiveOpaqueTokenIntrospector` `@Bean`
+
+Or, exposing a `ReactiveOpaqueTokenIntrospector` `@Bean` has the same effect as `introspector()`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveOpaqueTokenIntrospector introspector() {
+    return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun introspector(): ReactiveOpaqueTokenIntrospector {
+    return NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret)
+}
+----
+====
+
+[[webflux-oauth2resourceserver-opaque-authorization]]
+== Configuring Authorization
+
+An OAuth 2.0 Introspection endpoint will typically return a `scope` attribute, indicating the scopes (or authorities) it's been granted, for example:
+
+`{ ..., "scope" : "messages contacts"}`
+
+When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
+
+This means that to protect an endpoint or method with a scope derived from an Opaque Token, the corresponding expressions should include this prefix:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableWebFluxSecurity
+public class MappedAuthorities {
+    @Bean
+    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+        http
+            .authorizeExchange(exchange -> exchange
+                .pathMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
+                .pathMatchers("/messages/**").hasAuthority("SCOPE_messages")
+                .anyExchange().authenticated()
+            )
+            .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken);
+        return http.build();
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+    return http {
+        authorizeExchange {
+            authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
+            authorize("/messages/**", hasAuthority("SCOPE_messages"))
+            authorize(anyExchange, authenticated)
+        }
+        oauth2ResourceServer {
+            opaqueToken { }
+        }
+    }
+}
+----
+====
+
+Or similarly with method security:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@PreAuthorize("hasAuthority('SCOPE_messages')")
+public Flux<Message> getMessages(...) {}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@PreAuthorize("hasAuthority('SCOPE_messages')")
+fun getMessages(): Flux<Message> { }
+----
+====
+
+[[webflux-oauth2resourceserver-opaque-authorization-extraction]]
+=== Extracting Authorities Manually
+
+By default, Opaque Token support will extract the scope claim from an introspection response and parse it into individual `GrantedAuthority` instances.
+
+For example, if the introspection response were:
+
+[source,json]
+----
+{
+    "active" : true,
+    "scope" : "message:read message:write"
+}
+----
+
+Then Resource Server would generate an `Authentication` with two authorities, one for `message:read` and the other for `message:write`.
+
+This can, of course, be customized using a custom `ReactiveOpaqueTokenIntrospector` that takes a look at the attribute set and converts in its own way:
+
+====
+.Java
+[source,java,role="primary"]
+----
+public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
+    private ReactiveOpaqueTokenIntrospector delegate =
+            new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
+
+    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
+        return this.delegate.introspect(token)
+                .map(principal -> new DefaultOAuth2AuthenticatedPrincipal(
+                        principal.getName(), principal.getAttributes(), extractAuthorities(principal)));
+    }
+
+    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
+        List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
+        return scopes.stream()
+                .map(SimpleGrantedAuthority::new)
+                .collect(Collectors.toList());
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+class CustomAuthoritiesOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
+    private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
+    override fun introspect(token: String): Mono<OAuth2AuthenticatedPrincipal> {
+        return delegate.introspect(token)
+                .map { principal: OAuth2AuthenticatedPrincipal ->
+                    DefaultOAuth2AuthenticatedPrincipal(
+                            principal.name, principal.attributes, extractAuthorities(principal))
+                }
+    }
+
+    private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection<GrantedAuthority> {
+        val scopes = principal.getAttribute<List<String>>(OAuth2IntrospectionClaimNames.SCOPE)
+        return scopes
+                .map { SimpleGrantedAuthority(it) }
+    }
+}
+----
+====
+
+Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveOpaqueTokenIntrospector introspector() {
+    return new CustomAuthoritiesOpaqueTokenIntrospector();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun introspector(): ReactiveOpaqueTokenIntrospector {
+    return CustomAuthoritiesOpaqueTokenIntrospector()
+}
+----
+====
+
+[[webflux-oauth2resourceserver-opaque-jwt-introspector]]
+== Using Introspection with JWTs
+
+A common question is whether or not introspection is compatible with JWTs.
+Spring Security's Opaque Token support has been designed to not care about the format of the token -- it will gladly pass any token to the introspection endpoint provided.
+
+So, let's say that you've got a requirement that requires you to check with the authorization server on each request, in case the JWT has been revoked.
+
+Even though you are using the JWT format for the token, your validation method is introspection, meaning you'd want to do:
+
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        opaque-token:
+          introspection-uri: https://idp.example.org/introspection
+          client-id: client
+          client-secret: secret
+----
+
+In this case, the resulting `Authentication` would be `BearerTokenAuthentication`.
+Any attributes in the corresponding `OAuth2AuthenticatedPrincipal` would be whatever was returned by the introspection endpoint.
+
+But, let's say that, oddly enough, the introspection endpoint only returns whether or not the token is active.
+Now what?
+
+In this case, you can create a custom `ReactiveOpaqueTokenIntrospector` that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes:
+
+====
+.Java
+[source,java,role="primary"]
+----
+public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
+	private ReactiveOpaqueTokenIntrospector delegate =
+			new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
+	private ReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(new ParseOnlyJWTProcessor());
+
+	public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
+		return this.delegate.introspect(token)
+				.flatMap(principal -> this.jwtDecoder.decode(token))
+				.map(jwt -> new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES));
+	}
+
+	private static class ParseOnlyJWTProcessor implements Converter<JWT, Mono<JWTClaimsSet>> {
+		public Mono<JWTClaimsSet> convert(JWT jwt) {
+			try {
+				return Mono.just(jwt.getJWTClaimsSet());
+			} catch (Exception ex) {
+				return Mono.error(ex);
+			}
+		}
+	}
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+class JwtOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
+    private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
+    private val jwtDecoder: ReactiveJwtDecoder = NimbusReactiveJwtDecoder(ParseOnlyJWTProcessor())
+    override fun introspect(token: String): Mono<OAuth2AuthenticatedPrincipal> {
+        return delegate.introspect(token)
+                .flatMap { jwtDecoder.decode(token) }
+                .map { jwt: Jwt -> DefaultOAuth2AuthenticatedPrincipal(jwt.claims, NO_AUTHORITIES) }
+    }
+
+    private class ParseOnlyJWTProcessor : Converter<JWT, Mono<JWTClaimsSet>> {
+        override fun convert(jwt: JWT): Mono<JWTClaimsSet> {
+            return try {
+                Mono.just(jwt.jwtClaimsSet)
+            } catch (e: Exception) {
+                Mono.error(e)
+            }
+        }
+    }
+}
+----
+====
+
+Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+public ReactiveOpaqueTokenIntrospector introspector() {
+    return new JwtOpaqueTokenIntropsector();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun introspector(): ReactiveOpaqueTokenIntrospector {
+    return JwtOpaqueTokenIntrospector()
+}
+----
+====
+
+[[webflux-oauth2resourceserver-opaque-userinfo]]
+== Calling a `/userinfo` Endpoint
+
+Generally speaking, a Resource Server doesn't care about the underlying user, but instead about the authorities that have been granted.
+
+That said, at times it can be valuable to tie the authorization statement back to a user.
+
+If an application is also using `spring-security-oauth2-client`, having set up the appropriate `ClientRegistrationRepository`, then this is quite simple with a custom `OpaqueTokenIntrospector`.
+This implementation below does three things:
+
+* Delegates to the introspection endpoint, to affirm the token's validity
+* Looks up the appropriate client registration associated with the `/userinfo` endpoint
+* Invokes and returns the response from the `/userinfo` endpoint
+
+====
+.Java
+[source,java,role="primary"]
+----
+public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
+	private final ReactiveOpaqueTokenIntrospector delegate =
+			new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
+	private final ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService =
+			new DefaultReactiveOAuth2UserService();
+
+	private final ReactiveClientRegistrationRepository repository;
+
+	// ... constructor
+
+	@Override
+	public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
+		return Mono.zip(this.delegate.introspect(token), this.repository.findByRegistrationId("registration-id"))
+				.map(t -> {
+					OAuth2AuthenticatedPrincipal authorized = t.getT1();
+					ClientRegistration clientRegistration = t.getT2();
+					Instant issuedAt = authorized.getAttribute(ISSUED_AT);
+					Instant expiresAt = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT);
+					OAuth2AccessToken accessToken = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
+					return new OAuth2UserRequest(clientRegistration, accessToken);
+				})
+				.flatMap(this.oauth2UserService::loadUser);
+	}
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
+    private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
+    private val oauth2UserService: ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> = DefaultReactiveOAuth2UserService()
+    private val repository: ReactiveClientRegistrationRepository? = null
+
+    // ... constructor
+    override fun introspect(token: String?): Mono<OAuth2AuthenticatedPrincipal> {
+        return Mono.zip<OAuth2AuthenticatedPrincipal, ClientRegistration>(delegate.introspect(token), repository!!.findByRegistrationId("registration-id"))
+                .map<OAuth2UserRequest> { t: Tuple2<OAuth2AuthenticatedPrincipal, ClientRegistration> ->
+                    val authorized = t.t1
+                    val clientRegistration = t.t2
+                    val issuedAt: Instant? = authorized.getAttribute(ISSUED_AT)
+                    val expiresAt: Instant? = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT)
+                    val accessToken = OAuth2AccessToken(BEARER, token, issuedAt, expiresAt)
+                    OAuth2UserRequest(clientRegistration, accessToken)
+                }
+                .flatMap { userRequest: OAuth2UserRequest -> oauth2UserService.loadUser(userRequest) }
+    }
+}
+----
+====
+
+If you aren't using `spring-security-oauth2-client`, it's still quite simple.
+You will simply need to invoke the `/userinfo` with your own instance of `WebClient`:
+
+====
+.Java
+[source,java,role="primary"]
+----
+public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
+    private final ReactiveOpaqueTokenIntrospector delegate =
+            new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
+    private final WebClient rest = WebClient.create();
+
+    @Override
+    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
+        return this.delegate.introspect(token)
+		        .map(this::makeUserInfoRequest);
+    }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
+    private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
+    private val rest: WebClient = WebClient.create()
+
+    override fun introspect(token: String): Mono<OAuth2AuthenticatedPrincipal> {
+        return delegate.introspect(token)
+                .map(this::makeUserInfoRequest)
+    }
+}
+----
+====
+
+Either way, having created your `ReactiveOpaqueTokenIntrospector`, you should publish it as a `@Bean` to override the defaults:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+ReactiveOpaqueTokenIntrospector introspector() {
+    return new UserInfoOpaqueTokenIntrospector();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun introspector(): ReactiveOpaqueTokenIntrospector {
+    return UserInfoOpaqueTokenIntrospector()
+}
+----
+====