863 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
			
		
		
	
	
			863 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
| = 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 Resource Server support is collected in `spring-security-oauth2-resource-server`.
 | |
| However, unless you provide a custom <<webflux-oauth2resourceserver-opaque-introspector-bean,`ReactiveOpaqueTokenIntrospector`>>, the Resource Server falls back to `ReactiveOpaqueTokenIntrospector`.
 | |
| This means that both `spring-security-oauth2-resource-server` and `oauth2-oidc-sdk` are necessary to have a working minimal Resource Server that supports opaque Bearer Tokens.
 | |
| See `spring-security-oauth2-resource-server` in order to determine the correct version for `oauth2-oidc-sdk`.
 | |
| 
 | |
| [[webflux-oauth2resourceserver-opaque-minimalconfiguration]]
 | |
| == Minimal Configuration for Introspection
 | |
| 
 | |
| Typically, you can verify an opaque token with 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 steps:
 | |
| 
 | |
| . Include the needed dependencies.
 | |
| . Indicate the introspection endpoint details.
 | |
| 
 | |
| [[webflux-oauth2resourceserver-opaque-introspectionuri]]
 | |
| === Specifying the Authorization Server
 | |
| 
 | |
| You can specify where the introspection endpoint is:
 | |
| 
 | |
| [source,yaml]
 | |
| ----
 | |
| spring:
 | |
|   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 uses these properties to further self-configure and subsequently validate incoming JWTs.
 | |
| 
 | |
| [NOTE]
 | |
| ====
 | |
| If the authorization server responses that the token is valid, then it is.
 | |
| ====
 | |
| 
 | |
| === Startup Expectations
 | |
| 
 | |
| When this property and these dependencies are used, Resource Server automatically configures 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 has started, Resource Server tries 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 tries to process the request according to the Bearer Token specification.
 | |
| 
 | |
| Given an Opaque Token, Resource Server:
 | |
| 
 | |
| . Queries the provided introspection endpoint by using the provided credentials and the token.
 | |
| . Inspects the response for an `{ 'active' : true }` attribute.
 | |
| . Maps each scope to an authority with a prefix of `SCOPE_`.
 | |
| 
 | |
| By default, the resulting `Authentication#getPrincipal` is a Spring Security javadoc:org.springframework.security.oauth2.core.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>>
 | |
| * <<webflux-oauth2resourceserver-opaque-authorization-extraction>>
 | |
| * <<webflux-oauth2resourceserver-opaque-jwt-introspector>>
 | |
| 
 | |
| [[webflux-oauth2resourceserver-opaque-attributes]]
 | |
| == Looking Up Attributes After Authentication
 | |
| 
 | |
| Once a token is authenticated, an instance of `BearerTokenAuthentication` is set in the `SecurityContext`.
 | |
| 
 | |
| This means that it is available in `@Controller` methods when you use `@EnableWebFlux` in your configuration:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| 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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| 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 with SpEL
 | |
| 
 | |
| You can access attributes with the Spring Expression Language (SpEL).
 | |
| 
 | |
| For example, if you use `@EnableReactiveMethodSecurity` so that you can use `@PreAuthorize` annotations, you can do:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| 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
 | |
| 
 | |
| Spring Boot generates two `@Bean` instances for Resource Server.
 | |
| 
 | |
| The first is a `SecurityWebFilterChain` that configures the application as a resource server.
 | |
| When you use an Opaque Token, this `SecurityWebFilterChain` looks like:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| 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 does not expose a `SecurityWebFilterChain` bean, Spring Boot exposes the default bean (shown in the preceding listing).
 | |
| 
 | |
| You can replace it by exposing the bean within the application:
 | |
| 
 | |
| .Replacing SecurityWebFilterChain
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
 | |
| 
 | |
| @Configuration
 | |
| @EnableWebFluxSecurity
 | |
| public class MyCustomSecurityConfiguration {
 | |
|     @Bean
 | |
|     SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
 | |
|         http
 | |
|             .authorizeExchange(exchanges -> exchanges
 | |
|                 .pathMatchers("/messages/**").access(hasScope("message:read"))
 | |
|                 .anyExchange().authenticated()
 | |
|             )
 | |
|             .oauth2ResourceServer(oauth2 -> oauth2
 | |
|                 .opaqueToken(opaqueToken -> opaqueToken
 | |
|                     .introspector(myIntrospector())
 | |
|                 )
 | |
|             );
 | |
|         return http.build();
 | |
|     }
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope
 | |
| 
 | |
| @Bean
 | |
| fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
 | |
|     return http {
 | |
|         authorizeExchange {
 | |
|             authorize("/messages/**", hasScope("message:read"))
 | |
|             authorize(anyExchange, authenticated)
 | |
|         }
 | |
|         oauth2ResourceServer {
 | |
|             opaqueToken {
 | |
|                 introspector = myIntrospector()
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| The preceding example requires the scope of `message:read` for any URL that starts with `/messages/`.
 | |
| 
 | |
| Methods on the `oauth2ResourceServer` DSL 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`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| public ReactiveOpaqueTokenIntrospector introspector() {
 | |
|     return SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
 | |
|         .clientId(clientId).clientSecret(clientSecret).build();
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Bean
 | |
| fun introspector(): ReactiveOpaqueTokenIntrospector {
 | |
|     return SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
 | |
|         .clientId(clientId).clientSecret(clientSecret).build()
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| If the application does not expose a `ReactiveOpaqueTokenIntrospector` bean, Spring Boot exposes the default one (shown in the preceding listing).
 | |
| 
 | |
| You can override its configuration by using `introspectionUri()` and `introspectionClientCredentials()` or replace it by using `introspector()`.
 | |
| 
 | |
| [[webflux-oauth2resourceserver-opaque-introspectionuri-dsl]]
 | |
| === Using `introspectionUri()`
 | |
| 
 | |
| You can configure an authorization server's Introspection URI <<webflux-oauth2resourceserver-opaque-introspectionuri,as a configuration property>>, or you can supply in the DSL:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Configuration
 | |
| @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()`
 | |
| 
 | |
| `introspector()` is more powerful than `introspectionUri()`. It completely replaces any Boot auto-configuration of `ReactiveOpaqueTokenIntrospector`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Configuration
 | |
| @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, such as <<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()`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| public ReactiveOpaqueTokenIntrospector introspector() {
 | |
|     return SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
 | |
|         .clientId(clientId).clientSecret(clientSecret).build()
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Bean
 | |
| fun introspector(): ReactiveOpaqueTokenIntrospector {
 | |
|     return SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
 | |
|         .clientId(clientId).clientSecret(clientSecret).build()
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| [[webflux-oauth2resourceserver-opaque-authorization]]
 | |
| == Configuring Authorization
 | |
| 
 | |
| An OAuth 2.0 Introspection endpoint typically returns a `scope` attribute, indicating the scopes (or authorities) it has been granted -- for example:
 | |
| 
 | |
| [source,json]
 | |
| ----
 | |
| { ..., "scope" : "messages contacts"}
 | |
| ----
 | |
| 
 | |
| When this is the case, Resource Server tries to coerce these scopes into a list of granted authorities, prefixing each scope with a 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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
 | |
| 
 | |
| @Configuration
 | |
| @EnableWebFluxSecurity
 | |
| public class MappedAuthorities {
 | |
|     @Bean
 | |
|     SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
 | |
|         http
 | |
|             .authorizeExchange(exchange -> exchange
 | |
|                 .pathMatchers("/contacts/**").access(hasScope("contacts"))
 | |
|                 .pathMatchers("/messages/**").access(hasScope("messages"))
 | |
|                 .anyExchange().authenticated()
 | |
|             )
 | |
|             .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken);
 | |
|         return http.build();
 | |
|     }
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope
 | |
| 
 | |
| @Bean
 | |
| fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
 | |
|     return http {
 | |
|         authorizeExchange {
 | |
|             authorize("/contacts/**", hasScope("contacts"))
 | |
|             authorize("/messages/**", hasScope("messages"))
 | |
|             authorize(anyExchange, authenticated)
 | |
|         }
 | |
|         oauth2ResourceServer {
 | |
|             opaqueToken { }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| You can do something similar with method security:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| 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 extracts the scope claim from an introspection response and parses it into individual `GrantedAuthority` instances.
 | |
| 
 | |
| Consider the following example:
 | |
| 
 | |
| [source,json]
 | |
| ----
 | |
| {
 | |
|     "active" : true,
 | |
|     "scope" : "message:read message:write"
 | |
| }
 | |
| ----
 | |
| 
 | |
| If the introspection response were as the preceding example shows, Resource Server would generate an `Authentication` with two authorities, one for `message:read` and the other for `message:write`.
 | |
| 
 | |
| You can customize behavior by using a custom `ReactiveOpaqueTokenIntrospector` that looks at the attribute set and converts in its own way:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
 | |
|     private ReactiveOpaqueTokenIntrospector delegate = SpringReactiveOpaqueTokenIntrospector
 | |
|             .withIntrospectionUri("https://idp.example.org/introspect")
 | |
|             .clientId("client").clientSecret("secret").build();
 | |
| 
 | |
|     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 = SpringReactiveOpaqueTokenIntrospector
 | |
|             .withIntrospectionUri("https://idp.example.org/introspect")
 | |
|             .clientId("client").clientSecret("secret").build()
 | |
|     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, you can configure this custom introspector by exposing it as a `@Bean`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| 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 gladly passes any token to the provided introspection endpoint.
 | |
| 
 | |
| So, suppose you need 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 would 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.
 | |
| 
 | |
| However, suppose that, for whatever reason, the introspection endpoint returns only 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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
 | |
| 	private ReactiveOpaqueTokenIntrospector delegate = SpringReactiveOpaqueTokenIntrospector
 | |
|             .withIntrospectionUri("https://idp.example.org/introspect")
 | |
|             .clientId("client").clientSecret("secret").build();
 | |
| 	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 = SpringReactiveOpaqueTokenIntrospector
 | |
|             .withIntrospectionUri("https://idp.example.org/introspect")
 | |
|             .clientId("client").clientSecret("secret").build()
 | |
|     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, you can configure this custom introspector by exposing it as a `@Bean`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| 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 does not care about the underlying user but, instead, cares 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 also uses `spring-security-oauth2-client`, having set up the appropriate `ClientRegistrationRepository`,  you can do so with a custom `OpaqueTokenIntrospector`.
 | |
| The implementation in the next listing 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.
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
 | |
| 	private final ReactiveOpaqueTokenIntrospector delegate = SpringReactiveOpaqueTokenIntrospector
 | |
|             .withIntrospectionUri("https://idp.example.org/introspect")
 | |
|             .clientId("client").clientSecret("secret").build();
 | |
| 	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 = SpringReactiveOpaqueTokenIntrospector
 | |
|             .withIntrospectionUri("https://idp.example.org/introspect")
 | |
|             .clientId("client").clientSecret("secret").build()
 | |
|     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`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
 | |
|     private final ReactiveOpaqueTokenIntrospector delegate = SpringReactiveOpaqueTokenIntrospector
 | |
|             .withIntrospectionUri("https://idp.example.org/introspect")
 | |
|             .clientId("client").clientSecret("secret").build();
 | |
|     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 = SpringReactiveOpaqueTokenIntrospector
 | |
|             .withIntrospectionUri("https://idp.example.org/introspect")
 | |
|             .clientId("client").clientSecret("secret").build()
 | |
|     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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| @Bean
 | |
| ReactiveOpaqueTokenIntrospector introspector() {
 | |
|     return new UserInfoOpaqueTokenIntrospector();
 | |
| }
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| @Bean
 | |
| fun introspector(): ReactiveOpaqueTokenIntrospector {
 | |
|     return UserInfoOpaqueTokenIntrospector()
 | |
| }
 | |
| ----
 | |
| ======
 |