From 4b0e74aac4d56e0cfd641432e58854464993b8ce Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 26 Oct 2021 15:59:03 -0600 Subject: [PATCH] Separate Resource Server Servlet Docs Issue gh-10367 --- docs/modules/ROOT/nav.adoc | 6 +- .../reactive/oauth2/resource-server.adoc | 2 +- .../ROOT/pages/servlet/oauth2/index.adoc | 5 +- .../servlet/oauth2/oauth2-resourceserver.adoc | 3058 ----------------- .../oauth2/resource-server/bearer-tokens.adoc | 290 ++ .../servlet/oauth2/resource-server/index.adoc | 58 + .../servlet/oauth2/resource-server/jwt.adoc | 1380 ++++++++ .../oauth2/resource-server/multitenancy.adoc | 445 +++ .../oauth2/resource-server/opaque-token.adoc | 891 +++++ 9 files changed, 3072 insertions(+), 3063 deletions(-) delete mode 100644 docs/modules/ROOT/pages/servlet/oauth2/oauth2-resourceserver.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 8197b079e9..4c27690898 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -57,7 +57,11 @@ ** xref:servlet/oauth2/index.adoc[OAuth2] *** xref:servlet/oauth2/oauth2-login.adoc[OAuth2 Log In] *** xref:servlet/oauth2/oauth2-client.adoc[OAuth2 Client] -*** xref:servlet/oauth2/oauth2-resourceserver.adoc[OAuth2 Resource Server] +*** xref:servlet/oauth2/resource-server/index.adoc[OAuth2 Resource Server] +**** xref:servlet/oauth2/resource-server/jwt.adoc[JWT] +**** xref:servlet/oauth2/resource-server/opaque-token.adoc[Opaque Token] +**** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy] +**** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens] ** xref:servlet/saml2/index.adoc[SAML2] ** xref:servlet/exploits/index.adoc[Protection Against Exploits] *** xref:servlet/exploits/csrf.adoc[] diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server.adoc index 68ad9539b9..907e3b502d 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server.adoc @@ -897,7 +897,7 @@ fun jwtDecoder(): ReactiveJwtDecoder { [[webflux-oauth2resourceserver-opaque-minimaldependencies]] === Minimal Dependencies for Introspection -As described in xref:servlet/oauth2/oauth2-resourceserver.adoc#oauth2resourceserver-jwt-minimaldependencies[Minimal Dependencies for JWT] most of Resource Server support is collected in `spring-security-oauth2-resource-server`. +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 <> 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`. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/index.adoc index df50a690b6..abcb2591d8 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/index.adoc @@ -1,8 +1,7 @@ = OAuth2 +:page-section-summary-toc: 1 Spring Security provides comprehensive OAuth 2 support. This section discusses how to integrate OAuth 2 into your servlet based application. -* xref:servlet/oauth2/oauth2-login.adoc[] -* xref:servlet/oauth2/oauth2-client.adoc[] -* xref:servlet/oauth2/oauth2-resourceserver.adoc[] + diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-resourceserver.adoc b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-resourceserver.adoc deleted file mode 100644 index d9b584188c..0000000000 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-resourceserver.adoc +++ /dev/null @@ -1,3058 +0,0 @@ -[[oauth2resourceserver]] -= OAuth 2.0 Resource Server -:figures: servlet/oauth2 - -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. - -This section provides details on how Spring Security provides support for OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]. - -[NOTE] -==== -Working samples for both {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/jwe[JWTs] and {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security Samples repository]. -==== - -Let's take a look at how Bearer Token Authentication works within Spring Security. -First, we see that, like xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client. - -.Sending WWW-Authenticate Header -image::{figures}/bearerauthenticationentrypoint.png[] - -The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram. - -image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized. - -image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. - -image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__. -The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`] which sends a WWW-Authenticate header. -The `RequestCache` is typically a `NullRequestCache` that does not save the request since the client is capable of replaying the requests it originally requested. - -When a client receives the `WWW-Authenticate: Bearer` header, it knows it should retry with a bearer token. -Below is the flow for the bearer token being processed. - -[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]] -.Authenticating Bearer Token -image::{figures}/bearertokenauthenticationfilter.png[] - -The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram. - -image:{icondir}/number_1.png[] When the user submits their bearer token, the `BearerTokenAuthenticationFilter` creates a `BearerTokenAuthenticationToken` which is a type of xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] by extracting the token from the `HttpServletRequest`. - -image:{icondir}/number_2.png[] Next, the `HttpServletRequest` is passed to the `AuthenticationManagerResolver`, which selects the `AuthenticationManager`. The `BearerTokenAuthenticationToken` is passed into the `AuthenticationManager` to be authenticated. -The details of what `AuthenticationManager` looks like depends on whether you're configured for <> or <>. - -image:{icondir}/number_3.png[] If authentication fails, then __Failure__ - -* The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder] is cleared out. -* The `AuthenticationEntryPoint` is invoked to trigger the WWW-Authenticate header to be sent again. - -image:{icondir}/number_4.png[] If authentication is successful, then __Success__. - -* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. -* The `BearerTokenAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic. - -[[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. - -[[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. Query the Provider Configuration or Authorization Server Metadata endpoint for the `jwks_url` property -2. Query the `jwks_url` endpoint for supported algorithms -3. Configure the validation strategy to query `jwks_url` for valid public keys of the algorithms found -4. 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 JWT -2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `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 JWTs. - -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: - -* <> -* <> -* <> - -[[oauth2resourceserver-jwt-architecture]] -== How JWT Authentication Works - -Next, let's see the architectural components that Spring Security uses to support https://tools.ietf.org/html/rfc7519[JWT] Authentication in servlet-based applications, like the one we just saw. - -{security-api-url}org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.html[`JwtAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> and <> to authenticate a JWT. - -Let's take a look at how `JwtAuthenticationProvider` works within Spring Security. -The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <> works. - -.`JwtAuthenticationProvider` Usage -image::{figures}/jwtauthenticationprovider.png[] - -image:{icondir}/number_1.png[] The authentication `Filter` from <> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. - -image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `JwtAuthenticationProvider`. - -[[oauth2resourceserver-jwt-architecture-jwtdecoder]] -image:{icondir}/number_3.png[] `JwtAuthenticationProvider` decodes, verifies, and validates the `Jwt` using a <>. - -[[oauth2resourceserver-jwt-architecture-jwtauthenticationconverter]] -image:{icondir}/number_4.png[] `JwtAuthenticationProvider` then uses the <> to convert the `Jwt` into a `Collection` of granted authorities. - -image:{icondir}/number_5.png[] When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `JwtAuthenticationToken` and has a principal that is the `Jwt` returned by the configured `JwtDecoder`. -Ultimately, the returned `JwtAuthenticationToken` will be set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] by the authentication `Filter`. - -[[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 <>. - -[[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 `WebSecurityConfigurerAdapter` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `WebSecurityConfigurerAdapter` looks like: - -.Default JWT Configuration -==== -.Java -[source,java,role="primary"] ----- -protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { } - } - } -} ----- -==== - -If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one. - -Replacing this is as simple as exposing the bean within the application: - -.Custom JWT Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt - .jwtAuthenticationConverter(myConverter()) - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize("/messages/**", hasAuthority("SCOPE_message:read")) - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { - jwtAuthenticationConverter = myConverter() - } - } - } - } -} ----- -==== - -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. - -[[oauth2resourceserver-jwt-decoder]] -For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which <>: - -.JWT Decoder -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtDecoder jwtDecoder() { - return JwtDecoders.fromIssuerLocation(issuerUri); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - return JwtDecoders.fromIssuerLocation(issuerUri) -} ----- -==== - -[NOTE] -Calling `{security-api-url}org/springframework/security/oauth2/jwt/JwtDecoders.html#fromIssuerLocation-java.lang.String-[JwtDecoders#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 `JwtDecoder` bean, then Spring Boot will expose the above default one. - -And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`. - -Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a `JwtDecoder` can be specified in XML. - -The filter chain is specified like so: - -.Default JWT Configuration -==== -.Xml -[source,xml,role="primary"] ----- - - - - - - ----- -==== - -And the `JwtDecoder` like so: - -.JWT Decoder -==== -.Xml -[source,xml,role="primary"] ----- - - - ----- -==== - -[[oauth2resourceserver-jwt-jwkseturi-dsl]] -=== Using `jwkSetUri()` - -An authorization server's JWK Set Uri can be configured <> or it can be supplied in the DSL: - -.JWK Set Uri Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt - .jwkSetUri("https://idp.example.com/.well-known/jwks.json") - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { - jwkSetUri = "https://idp.example.com/.well-known/jwks.json" - } - } - } - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - ----- -==== - -Using `jwkSetUri()` takes precedence over any configuration property. - -[[oauth2resourceserver-jwt-decoder-dsl]] -=== Using `decoder()` - -More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of <>: - -.JWT Decoder Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt - .decoder(myCustomDecoder()) - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class DirectlyConfiguredJwtDecoder : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { - jwtDecoder = myCustomDecoder() - } - } - } - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - ----- -==== - -This is handy when deeper configuration, like <>, <>, or <>, is necessary. - -[[oauth2resourceserver-jwt-decoder-bean]] -=== Exposing a `JwtDecoder` `@Bean` - -Or, exposing a <> `@Bean` has the same effect as `decoder()`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() -} ----- -==== - -[[oauth2resourceserver-jwt-decoder-algorithm]] -== Configuring Trusted Algorithms - -By default, `NimbusJwtDecoder`, and hence Resource Server, will only trust and verify tokens using `RS256`. - -You can customize this via <>, <>, or from the <>. - -[[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 ----- - -[[oauth2resourceserver-jwt-decoder-builder]] -=== Using a Builder - -For greater power, though, we can use a builder that ships with `NimbusJwtDecoder`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsAlgorithm(RS512).build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsAlgorithm(RS512).build() -} ----- -==== - -Calling `jwsAlgorithm` more than once will configure `NimbusJwtDecoder` to trust more than one algorithm, like so: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build() -} ----- -==== - -Or, you can call `jwsAlgorithms`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsAlgorithms(algorithms -> { - algorithms.add(RS512); - algorithms.add(ES512); - }).build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsAlgorithms { - it.add(RS512) - it.add(ES512) - }.build() -} ----- -==== - -[[oauth2resourceserver-jwt-decoder-jwk-response]] -=== From JWK Set response - -Since Spring Security's JWT support is based off of Nimbus, you can use all it's great features as well. - -For example, Nimbus has a `JWSKeySelector` implementation that will select the set of algorithms based on the JWK Set URI response. -You can use it to generate a `NimbusJwtDecoder` like so: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtDecoder jwtDecoder() { - // makes a request to the JWK Set endpoint - JWSKeySelector jwsKeySelector = - JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl); - - DefaultJWTProcessor jwtProcessor = - new DefaultJWTProcessor<>(); - jwtProcessor.setJWSKeySelector(jwsKeySelector); - - return new NimbusJwtDecoder(jwtProcessor); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - // makes a request to the JWK Set endpoint - val jwsKeySelector: JWSKeySelector = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl) - val jwtProcessor: DefaultJWTProcessor = DefaultJWTProcessor() - jwtProcessor.jwsKeySelector = jwsKeySelector - return NimbusJwtDecoder(jwtProcessor) -} ----- -==== - -[[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 <> or by <>. - -[[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`: - -==== -.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 -> - beanFactory.getBean() - .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 ----- -==== - -[[oauth2resourceserver-jwt-decoder-public-key-builder]] -=== Using a Builder - -To wire an `RSAPublicKey` directly, you can simply use the appropriate `NimbusJwtDecoder` builder, like so: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withPublicKey(this.key).build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withPublicKey(this.key).build() -} ----- -==== - -[[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 `NimbusJwtDecoder` builder, like so: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withSecretKey(this.key).build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withSecretKey(key).build() -} ----- -==== - -[[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: - -.Authorization Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") - .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") - .anyRequest().authenticated() - ) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize("/contacts/**", hasAuthority("SCOPE_contacts")) - authorize("/messages/**", hasAuthority("SCOPE_messages")) - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { } - } - } - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - ----- -==== - -Or similarly with method security: - -==== -.Java -[source,java,role="primary"] ----- -@PreAuthorize("hasAuthority('SCOPE_messages')") -public List getMessages(...) {} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@PreAuthorize("hasAuthority('SCOPE_messages')") -fun getMessages(): List { } ----- -==== - -[[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, Spring Security ships with `JwtAuthenticationConverter`, which is responsible for <>. -By default, Spring Security will wire the `JwtAuthenticationProvider` with a default instance of `JwtAuthenticationConverter`. - -As part of configuring a `JwtAuthenticationConverter`, you can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities. - -Let's say that that your authorization server communicates authorities in a custom claim called `authorities`. -In that case, you can configure the claim that <> should inspect, like so: - -.Authorities Claim Configuration -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtAuthenticationConverter jwtAuthenticationConverter() { - JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); - - JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); - return jwtAuthenticationConverter; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtAuthenticationConverter(): JwtAuthenticationConverter { - val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter() - grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities") - - val jwtAuthenticationConverter = JwtAuthenticationConverter() - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter) - return jwtAuthenticationConverter -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - - - - - - - - - ----- -==== - -You can also configure the authority prefix to be different as well. -Instead of prefixing each authority with `SCOPE_`, you can change it to `ROLE_` like so: - -.Authorities Prefix Configuration -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtAuthenticationConverter jwtAuthenticationConverter() { - JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); - - JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); - return jwtAuthenticationConverter; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtAuthenticationConverter(): JwtAuthenticationConverter { - val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter() - grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_") - - val jwtAuthenticationConverter = JwtAuthenticationConverter() - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter) - return jwtAuthenticationConverter -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - - - - - - - - - ----- -==== - -Or, you can remove the prefix altogether by calling `JwtGrantedAuthoritiesConverter#setAuthorityPrefix("")`. - -For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter`: - -==== -.Java -[source,java,role="primary"] ----- -static class CustomAuthenticationConverter implements Converter { - public AbstractAuthenticationToken convert(Jwt jwt) { - return new CustomAuthenticationToken(jwt); - } -} - -// ... - -@EnableWebSecurity -public class CustomAuthenticationConverterConfig extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt - .jwtAuthenticationConverter(new CustomAuthenticationConverter()) - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -internal class CustomAuthenticationConverter : Converter { - override fun convert(jwt: Jwt): AbstractAuthenticationToken { - return CustomAuthenticationToken(jwt) - } -} - -// ... - -@EnableWebSecurity -class CustomAuthenticationConverterConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { - jwtAuthenticationConverter = CustomAuthenticationConverter() - } - } - } - } -} ----- -==== - -[[oauth2resourceserver-jwt-validation]] -== Configuring Validation - -Using <>, 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. - -[[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 -JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) - JwtDecoders.fromIssuerLocation(issuerUri); - - OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator<>( - new JwtTimestampValidator(Duration.ofSeconds(60)), - new JwtIssuerValidator(issuerUri)); - - jwtDecoder.setJwtValidator(withClockSkew); - - return jwtDecoder; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder - - val withClockSkew: OAuth2TokenValidator = DelegatingOAuth2TokenValidator( - JwtTimestampValidator(Duration.ofSeconds(60)), - JwtIssuerValidator(issuerUri)) - - jwtDecoder.setJwtValidator(withClockSkew) - - return jwtDecoder -} ----- -==== - -[NOTE] -By default, Resource Server configures a clock skew of 60 seconds. - -[[oauth2resourceserver-jwt-validation-custom]] -=== Configuring a Custom Validator - -Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API: - -==== -.Java -[source,java,role="primary"] ----- -OAuth2TokenValidator audienceValidator() { - return new JwtClaimValidator>(AUD, aud -> aud.contains("messaging")); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -fun audienceValidator(): OAuth2TokenValidator { - return JwtClaimValidator>(AUD) { aud -> aud.contains("messaging") } -} ----- -==== - -Or, for more control you can implement your own `OAuth2TokenValidator`: - -==== -.Java -[source,java,role="primary"] ----- -static class AudienceValidator implements OAuth2TokenValidator { - OAuth2Error error = new OAuth2Error("custom_code", "Custom error message", null); - - @Override - public OAuth2TokenValidatorResult validate(Jwt jwt) { - if (jwt.getAudience().contains("messaging")) { - return OAuth2TokenValidatorResult.success(); - } else { - return OAuth2TokenValidatorResult.failure(error); - } - } -} - -// ... - -OAuth2TokenValidator audienceValidator() { - return new AudienceValidator(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -internal class AudienceValidator : OAuth2TokenValidator { - var error: OAuth2Error = OAuth2Error("custom_code", "Custom error message", null) - - override fun validate(jwt: Jwt): OAuth2TokenValidatorResult { - return if (jwt.audience.contains("messaging")) { - OAuth2TokenValidatorResult.success() - } else { - OAuth2TokenValidatorResult.failure(error) - } - } -} - -// ... - -fun audienceValidator(): OAuth2TokenValidator { - return AudienceValidator() -} ----- -==== - -Then, to add into a resource server, it's a matter of specifying the <> instance: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) - JwtDecoders.fromIssuerLocation(issuerUri); - - OAuth2TokenValidator audienceValidator = audienceValidator(); - OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); - OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); - - jwtDecoder.setJwtValidator(withAudience); - - return jwtDecoder; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder - - val audienceValidator = audienceValidator() - val withIssuer: OAuth2TokenValidator = JwtValidators.createDefaultWithIssuer(issuerUri) - val withAudience: OAuth2TokenValidator = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator) - - jwtDecoder.setJwtValidator(withAudience) - - return jwtDecoder -} ----- -==== - -[[oauth2resourceserver-jwt-claimsetmapping]] -== Configuring Claim Set Mapping - -Spring Security uses the https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home[Nimbus] library for parsing JWTs and validating their signatures. -Consequently, Spring Security is subject to Nimbus's interpretation of each field value and how to coerce each into a Java type. - -For example, because Nimbus remains Java 7 compatible, it doesn't use `Instant` to represent timestamp fields. - -And it's entirely possible to use a different library or for JWT processing, which may make its own coercion decisions that need adjustment. - -Or, quite simply, a resource server may want to add or remove claims from a JWT for domain-specific reasons. - -For these purposes, Resource Server supports mapping the JWT claim set with `MappedJwtClaimSetConverter`. - -[[oauth2resourceserver-jwt-claimsetmapping-singleclaim]] -=== Customizing the Conversion of a Single Claim - -By default, `MappedJwtClaimSetConverter` will attempt to coerce claims into the following types: - -|============ -| Claim | Java Type -| `aud` | `Collection` -| `exp` | `Instant` -| `iat` | `Instant` -| `iss` | `String` -| `jti` | `String` -| `nbf` | `Instant` -| `sub` | `String` -|============ - -An individual claim's conversion strategy can be configured using `MappedJwtClaimSetConverter.withDefaults`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); - - MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter - .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub)); - jwtDecoder.setClaimSetConverter(converter); - - return jwtDecoder; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - val jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() - - val converter = MappedJwtClaimSetConverter - .withDefaults(mapOf("sub" to this::lookupUserIdBySub)) - jwtDecoder.setClaimSetConverter(converter) - - return jwtDecoder -} ----- -==== -This will keep all the defaults, except it will override the default claim converter for `sub`. - -[[oauth2resourceserver-jwt-claimsetmapping-add]] -=== Adding a Claim - -`MappedJwtClaimSetConverter` can also be used to add a custom claim, for example, to adapt to an existing system: - -==== -.Java -[source,java,role="primary"] ----- -MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value")); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter { "value" })) ----- -==== - -[[oauth2resourceserver-jwt-claimsetmapping-remove]] -=== Removing a Claim - -And removing a claim is also simple, using the same API: - -==== -.Java -[source,java,role="primary"] ----- -MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null)); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter { null })) ----- -==== - -[[oauth2resourceserver-jwt-claimsetmapping-rename]] -=== Renaming a Claim - -In more sophisticated scenarios, like consulting multiple claims at once or renaming a claim, Resource Server accepts any class that implements `Converter, Map>`: - -==== -.Java -[source,java,role="primary"] ----- -public class UsernameSubClaimAdapter implements Converter, Map> { - private final MappedJwtClaimSetConverter delegate = - MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); - - public Map convert(Map claims) { - Map convertedClaims = this.delegate.convert(claims); - - String username = (String) convertedClaims.get("user_name"); - convertedClaims.put("sub", username); - - return convertedClaims; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -class UsernameSubClaimAdapter : Converter, Map> { - private val delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()) - override fun convert(claims: Map): Map { - val convertedClaims = delegate.convert(claims) - val username = convertedClaims["user_name"] as String - convertedClaims["sub"] = username - return convertedClaims - } -} ----- -==== - -And then, the instance can be supplied like normal: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); - jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter()); - return jwtDecoder; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() - jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter()) - return jwtDecoder -} ----- -==== - -[[oauth2resourceserver-jwt-timeouts]] -== Configuring Timeouts - -By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server. - -This may be too short in some scenarios. -Further, it doesn't take into account more sophisticated patterns like back-off and discovery. - -To adjust the way in which Resource Server connects to the authorization server, `NimbusJwtDecoder` accepts an instance of `RestOperations`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { - RestOperations rest = builder - .setConnectTimeout(Duration.ofSeconds(60)) - .setReadTimeout(Duration.ofSeconds(60)) - .build(); - - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build(); - return jwtDecoder; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder { - val rest: RestOperations = builder - .setConnectTimeout(Duration.ofSeconds(60)) - .setReadTimeout(Duration.ofSeconds(60)) - .build() - return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build() -} ----- -==== - -Also by default, Resource Server caches in-memory the authorization server's JWK set for 5 minutes, which you may want to adjust. -Further, it doesn't take into account more sophisticated caching patterns like eviction or using a shared cache. - -To adjust the way in which Resource Server caches the JWK set, `NimbusJwtDecoder` accepts an instance of `Cache`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public JwtDecoder jwtDecoder(CacheManager cacheManager) { - return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) - .cache(cacheManager.getCache("jwks")) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(cacheManager: CacheManager): JwtDecoder { - return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) - .cache(cacheManager.getCache("jwks")) - .build() -} ----- -==== - -When given a `Cache`, Resource Server will use the JWK Set Uri as the key and the JWK Set JSON as the value. - -NOTE: Spring isn't a cache provider, so you'll need to make sure to include the appropriate dependencies, like `spring-boot-starter-cache` and your favorite caching provider. - -NOTE: Whether it's socket or cache timeouts, you may instead want to work with Nimbus directly. -To do so, remember that `NimbusJwtDecoder` ships with a constructor that takes Nimbus's `JWTProcessor`. - -[[oauth2resourceserver-opaque-minimaldependencies]] -== Minimal Dependencies for Introspection -As described in <> most of Resource Server support is collected in `spring-security-oauth2-resource-server`. -However unless a custom <> is provided, the Resource Server will fallback to NimbusOpaqueTokenIntrospector. -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`. - -[[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. - -[[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: - -* <> -* <> -* <> -* <> - -[[oauth2resourceserver-opaque-architecture]] -== How Opaque Token Authentication Works - -Next, let's see the architectural components that Spring Security uses to support https://tools.ietf.org/html/rfc7662[opaque token] Authentication in servlet-based applications, like the one we just saw. - -{security-api-url}org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.html[`OpaqueTokenAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> to authenticate an opaque token. - -Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring Security. -The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <> works. - -.`OpaqueTokenAuthenticationProvider` Usage -image::{figures}/opaquetokenauthenticationprovider.png[] - -image:{icondir}/number_1.png[] The authentication `Filter` from <> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. - -image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `OpaqueTokenAuthenticationProvider`. - -[[oauth2resourceserver-opaque-architecture-introspector]] -image:{icondir}/number_3.png[] `OpaqueTokenAuthenticationProvider` introspects the opaque token and adds granted authorities using an <>. -When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `BearerTokenAuthentication` and has a principal that is the `OAuth2AuthenticatedPrincipal` returned by the configured <>. -Ultimately, the returned `BearerTokenAuthentication` will be set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] by the authentication `Filter`. - -[[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 `@EnableWebMvc` in your configuration: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/foo") -public String foo(BearerTokenAuthentication authentication) { - return authentication.getTokenAttributes().get("sub") + " is the subject"; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/foo") -fun foo(authentication: BearerTokenAuthentication): String { - return 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 String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) { - return principal.getAttribute("sub") + " is the subject"; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/foo") -fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): String { - return principal.getAttribute("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 `@EnableGlobalMethodSecurity` so that you can use `@PreAuthorize` annotations, you can do: - -==== -.Java -[source,java,role="primary"] ----- -@PreAuthorize("principal?.attributes['sub'] == 'foo'") -public String forFoosEyesOnly() { - return "foo"; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@PreAuthorize("principal?.attributes['sub'] == 'foo'") -fun forFoosEyesOnly(): String { - return "foo" -} ----- -==== - -[[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 `WebSecurityConfigurerAdapter` that configures the app as a resource server. -When use Opaque Token, this `WebSecurityConfigurerAdapter` looks like: - -.Default Opaque Token Configuration -==== -.Java -[source,java,role="primary"] ----- -protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - opaqueToken { } - } - } -} ----- -==== - -If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one. - -Replacing this is as simple as exposing the bean within the application: - -.Custom Opaque Token Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .opaqueToken(opaqueToken -> opaqueToken - .introspector(myIntrospector()) - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize("/messages/**", hasAuthority("SCOPE_message:read")) - authorize(anyRequest, 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. - -[[oauth2resourceserver-opaque-introspector]] -For example, the second `@Bean` Spring Boot creates is an `OpaqueTokenIntrospector`, <>: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OpaqueTokenIntrospector introspector() { - return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun introspector(): OpaqueTokenIntrospector { - return NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret) -} ----- -==== - -If the application doesn't expose a <> bean, then Spring Boot will expose the above default one. - -And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`. - -Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a <> can be specified in XML. - -The filter chain is specified like so: - -.Default Opaque Token Configuration -==== -.Xml -[source,xml,role="primary"] ----- - - - - - - ----- -==== - -And the <> like so: - -.Opaque Token Introspector -==== -.Xml -[source,xml,role="primary"] ----- - - - - - ----- -==== - -[[oauth2resourceserver-opaque-introspectionuri-dsl]] -=== Using `introspectionUri()` - -An authorization server's Introspection Uri can be configured <> or it can be supplied in the DSL: - -.Introspection URI Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .opaqueToken(opaqueToken -> opaqueToken - .introspectionUri("https://idp.example.com/introspect") - .introspectionClientCredentials("client", "secret") - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class DirectlyConfiguredIntrospectionUri : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - opaqueToken { - introspectionUri = "https://idp.example.com/introspect" - introspectionClientCredentials("client", "secret") - } - } - } - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - ----- -==== - -Using `introspectionUri()` takes precedence over any configuration property. - -[[oauth2resourceserver-opaque-introspector-dsl]] -=== Using `introspector()` - -More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of <>: - -.Introspector Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .opaqueToken(opaqueToken -> opaqueToken - .introspector(myCustomIntrospector()) - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class DirectlyConfiguredIntrospector : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - opaqueToken { - introspector = myCustomIntrospector() - } - } - } - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - ----- -==== - -This is handy when deeper configuration, like <>, <>, or <>, is necessary. - -[[oauth2resourceserver-opaque-introspector-bean]] -=== Exposing a `OpaqueTokenIntrospector` `@Bean` - -Or, exposing a <> `@Bean` has the same effect as `introspector()`: - -[source,java] ----- -@Bean -public OpaqueTokenIntrospector introspector() { - return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); -} ----- - -[[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: - -.Authorization Opaque Token Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class MappedAuthorities extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorizeRequests -> authorizeRequests - .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") - .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") - .anyRequest().authenticated() - ) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class MappedAuthorities : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize("/contacts/**", hasAuthority("SCOPE_contacts")) - authorize("/messages/**", hasAuthority("SCOPE_messages")) - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - opaqueToken { } - } - } - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - ----- -==== - -Or similarly with method security: - -==== -.Java -[source,java,role="primary"] ----- -@PreAuthorize("hasAuthority('SCOPE_messages')") -public List getMessages(...) {} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@PreAuthorize("hasAuthority('SCOPE_messages')") -fun getMessages(): List {} ----- -==== - -[[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 <> that takes a look at the attribute set and converts in its own way: - -==== -.Java -[source,java,role="primary"] ----- -public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector { - private OpaqueTokenIntrospector delegate = - new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); - - public OAuth2AuthenticatedPrincipal introspect(String token) { - OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token); - return new DefaultOAuth2AuthenticatedPrincipal( - principal.getName(), principal.getAttributes(), extractAuthorities(principal)); - } - - private Collection extractAuthorities(OAuth2AuthenticatedPrincipal principal) { - List scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE); - return scopes.stream() - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -class CustomAuthoritiesOpaqueTokenIntrospector : OpaqueTokenIntrospector { - private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") - override fun introspect(token: String): OAuth2AuthenticatedPrincipal { - val principal: OAuth2AuthenticatedPrincipal = delegate.introspect(token) - return DefaultOAuth2AuthenticatedPrincipal( - principal.name, principal.attributes, extractAuthorities(principal)) - } - - private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection { - val scopes: List = principal.getAttribute(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 OpaqueTokenIntrospector introspector() { - return new CustomAuthoritiesOpaqueTokenIntrospector(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun introspector(): OpaqueTokenIntrospector { - return CustomAuthoritiesOpaqueTokenIntrospector() -} ----- -==== - -[[oauth2resourceserver-opaque-timeouts]] -== Configuring Timeouts - -By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server. - -This may be too short in some scenarios. -Further, it doesn't take into account more sophisticated patterns like back-off and discovery. - -To adjust the way in which Resource Server connects to the authorization server, `NimbusOpaqueTokenIntrospector` accepts an instance of `RestOperations`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder, OAuth2ResourceServerProperties properties) { - RestOperations rest = builder - .basicAuthentication(properties.getOpaquetoken().getClientId(), properties.getOpaquetoken().getClientSecret()) - .setConnectTimeout(Duration.ofSeconds(60)) - .setReadTimeout(Duration.ofSeconds(60)) - .build(); - - return new NimbusOpaqueTokenIntrospector(introspectionUri, rest); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun introspector(builder: RestTemplateBuilder, properties: OAuth2ResourceServerProperties): OpaqueTokenIntrospector? { - val rest: RestOperations = builder - .basicAuthentication(properties.opaquetoken.clientId, properties.opaquetoken.clientSecret) - .setConnectTimeout(Duration.ofSeconds(60)) - .setReadTimeout(Duration.ofSeconds(60)) - .build() - return NimbusOpaqueTokenIntrospector(introspectionUri, rest) -} ----- -==== - -[[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 <> 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 OpaqueTokenIntrospector { - private OpaqueTokenIntrospector delegate = - new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); - private JwtDecoder jwtDecoder = new NimbusJwtDecoder(new ParseOnlyJWTProcessor()); - - public OAuth2AuthenticatedPrincipal introspect(String token) { - OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token); - try { - Jwt jwt = this.jwtDecoder.decode(token); - return new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES); - } catch (JwtException ex) { - throw new OAuth2IntrospectionException(ex); - } - } - - private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor { - JWTClaimsSet process(SignedJWT jwt, SecurityContext context) - throws JOSEException { - return jwt.getJWTClaimsSet(); - } - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -class JwtOpaqueTokenIntrospector : OpaqueTokenIntrospector { - private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") - private val jwtDecoder: JwtDecoder = NimbusJwtDecoder(ParseOnlyJWTProcessor()) - override fun introspect(token: String): OAuth2AuthenticatedPrincipal { - val principal = delegate.introspect(token) - return try { - val jwt: Jwt = jwtDecoder.decode(token) - DefaultOAuth2AuthenticatedPrincipal(jwt.claims, NO_AUTHORITIES) - } catch (ex: JwtException) { - throw OAuth2IntrospectionException(ex.message) - } - } - - private class ParseOnlyJWTProcessor : DefaultJWTProcessor() { - override fun process(jwt: SignedJWT, context: SecurityContext): JWTClaimsSet { - return jwt.jwtClaimsSet - } - } -} ----- -==== - -Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OpaqueTokenIntrospector introspector() { - return new JwtOpaqueTokenIntrospector(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun introspector(): OpaqueTokenIntrospector { - return JwtOpaqueTokenIntrospector() -} ----- -==== - -[[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 <>. -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 OpaqueTokenIntrospector { - private final OpaqueTokenIntrospector delegate = - new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); - private final OAuth2UserService oauth2UserService = new DefaultOAuth2UserService(); - - private final ClientRegistrationRepository repository; - - // ... constructor - - @Override - public OAuth2AuthenticatedPrincipal introspect(String token) { - OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token); - Instant issuedAt = authorized.getAttribute(ISSUED_AT); - Instant expiresAt = authorized.getAttribute(EXPIRES_AT); - ClientRegistration clientRegistration = this.repository.findByRegistrationId("registration-id"); - OAuth2AccessToken token = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt); - OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(clientRegistration, token); - return this.oauth2UserService.loadUser(oauth2UserRequest); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -class UserInfoOpaqueTokenIntrospector : OpaqueTokenIntrospector { - private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") - private val oauth2UserService = DefaultOAuth2UserService() - private val repository: ClientRegistrationRepository? = null - - // ... constructor - - override fun introspect(token: String): OAuth2AuthenticatedPrincipal { - val authorized = delegate.introspect(token) - val issuedAt: Instant? = authorized.getAttribute(ISSUED_AT) - val expiresAt: Instant? = authorized.getAttribute(EXPIRES_AT) - val clientRegistration: ClientRegistration = repository!!.findByRegistrationId("registration-id") - val accessToken = OAuth2AccessToken(BEARER, token, issuedAt, expiresAt) - val oauth2UserRequest = OAuth2UserRequest(clientRegistration, accessToken) - return oauth2UserService.loadUser(oauth2UserRequest) - } -} ----- -==== - -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 OpaqueTokenIntrospector { - private final OpaqueTokenIntrospector delegate = - new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); - private final WebClient rest = WebClient.create(); - - @Override - public OAuth2AuthenticatedPrincipal introspect(String token) { - OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token); - return makeUserInfoRequest(authorized); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -class UserInfoOpaqueTokenIntrospector : OpaqueTokenIntrospector { - private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") - private val rest: WebClient = WebClient.create() - - override fun introspect(token: String): OAuth2AuthenticatedPrincipal { - val authorized = delegate.introspect(token) - return makeUserInfoRequest(authorized) - } -} ----- -==== - -Either way, having created your <>, you should publish it as a `@Bean` to override the defaults: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -OpaqueTokenIntrospector introspector() { - return new UserInfoOpaqueTokenIntrospector(...); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun introspector(): OpaqueTokenIntrospector { - return UserInfoOpaqueTokenIntrospector(...) -} ----- -==== - -[[oauth2reourceserver-opaqueandjwt]] -== Supporting both JWT and Opaque Token - -In some cases, you may have a need to access both kinds of tokens. -For example, you may support more than one tenant where one tenant issues JWTs and the other issues opaque tokens. - -If this decision must be made at request-time, then you can use an `AuthenticationManagerResolver` to achieve it, like so: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -AuthenticationManagerResolver tokenAuthenticationManagerResolver - (JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) { - AuthenticationManager jwt = new ProviderManager(new JwtAuthenticationProvider(jwtDecoder)); - AuthenticationManager opaqueToken = new ProviderManager( - new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)); - return (request) -> useJwt(request) ? jwt : opaqueToken; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun tokenAuthenticationManagerResolver - (jwtDecoder: JwtDecoder, opaqueTokenIntrospector: OpaqueTokenIntrospector): - AuthenticationManagerResolver { - val jwt = ProviderManager(JwtAuthenticationProvider(jwtDecoder)) - val opaqueToken = ProviderManager(OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)); - - return AuthenticationManagerResolver { request -> - if (useJwt(request)) { - jwt - } else { - opaqueToken - } - } -} ----- -==== - -NOTE: The implementation of `useJwt(HttpServletRequest)` will likely depend on custom request material like the path. - -And then specify this `AuthenticationManagerResolver` in the DSL: - -.Authentication Manager Resolver -==== -.Java -[source,java,role="primary"] ----- -http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .authenticationManagerResolver(this.tokenAuthenticationManagerResolver) - ); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - authenticationManagerResolver = tokenAuthenticationManagerResolver() - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - ----- -==== - -[[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 `JwtIssuerAuthenticationManagerResolver`, like so: - -.Multitenancy Tenant by JWT Claim -==== -.Java -[source,java,role="primary"] ----- -JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver - ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); - -http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .authenticationManagerResolver(authenticationManagerResolver) - ); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val customAuthenticationManagerResolver = JwtIssuerAuthenticationManagerResolver - ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo") -http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - authenticationManagerResolver = customAuthenticationManagerResolver - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - - https://idp.example.org/issuerOne - https://idp.example.org/issuerTwo - - - ----- -==== - -This is nice because the issuer endpoints are loaded lazily. -In fact, the corresponding `JwtAuthenticationProvider` 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 `JwtIssuerAuthenticationManagerResolver` with a repository of `AuthenticationManager` instances, which you can edit at runtime, like so: - -==== -.Java -[source,java,role="primary"] ----- -private void addManager(Map authenticationManagers, String issuer) { - JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider - (JwtDecoders.fromIssuerLocation(issuer)); - authenticationManagers.put(issuer, authenticationProvider::authenticate); -} - -// ... - -JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = - new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get); - -http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .authenticationManagerResolver(authenticationManagerResolver) - ); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -private fun addManager(authenticationManagers: MutableMap, issuer: String) { - val authenticationProvider = JwtAuthenticationProvider(JwtDecoders.fromIssuerLocation(issuer)) - authenticationManagers[issuer] = AuthenticationManager { - authentication: Authentication? -> authenticationProvider.authenticate(authentication) - } -} - -// ... - -val customAuthenticationManagerResolver: JwtIssuerAuthenticationManagerResolver = - JwtIssuerAuthenticationManagerResolver(authenticationManagers::get) -http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - authenticationManagerResolver = customAuthenticationManagerResolver - } -} ----- -==== - -In this case, you construct `JwtIssuerAuthenticationManagerResolver` with a strategy for obtaining the `AuthenticationManager` 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 `AuthenticationManager` from it. -The issuer should be one that the code can verify from a trusted source like a list of allowed issuers. - -==== Parsing the Claim Only Once - -You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the <> later on in the request. - -This extra parsing can be alleviated by configuring the <> directly with a `JWTClaimsSetAwareJWSKeySelector` from Nimbus: - -==== -.Java -[source,java,role="primary"] ----- -@Component -public class TenantJWSKeySelector - implements JWTClaimsSetAwareJWSKeySelector { - - private final TenantRepository tenants; <1> - private final Map> selectors = new ConcurrentHashMap<>(); <2> - - public TenantJWSKeySelector(TenantRepository tenants) { - this.tenants = tenants; - } - - @Override - public List selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext) - throws KeySourceException { - return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant) - .selectJWSKeys(jwsHeader, securityContext); - } - - private String toTenant(JWTClaimsSet claimSet) { - return (String) claimSet.getClaim("iss"); - } - - private JWSKeySelector fromTenant(String tenant) { - return Optional.ofNullable(this.tenantRepository.findById(tenant)) <3> - .map(t -> t.getAttrbute("jwks_uri")) - .map(this::fromUri) - .orElseThrow(() -> new IllegalArgumentException("unknown tenant")); - } - - private JWSKeySelector fromUri(String uri) { - try { - return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); <4> - } catch (Exception ex) { - throw new IllegalArgumentException(ex); - } - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Component -class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimsSetAwareJWSKeySelector { - private val tenants: TenantRepository <1> - private val selectors: MutableMap> = ConcurrentHashMap() <2> - - init { - this.tenants = tenants - } - - fun selectKeys(jwsHeader: JWSHeader?, jwtClaimsSet: JWTClaimsSet, securityContext: SecurityContext): List { - return selectors.computeIfAbsent(toTenant(jwtClaimsSet)) { tenant: String -> fromTenant(tenant) } - .selectJWSKeys(jwsHeader, securityContext) - } - - private fun toTenant(claimSet: JWTClaimsSet): String { - return claimSet.getClaim("iss") as String - } - - private fun fromTenant(tenant: String): JWSKeySelector { - return Optional.ofNullable(this.tenants.findById(tenant)) <3> - .map { t -> t.getAttrbute("jwks_uri") } - .map { uri: String -> fromUri(uri) } - .orElseThrow { IllegalArgumentException("unknown tenant") } - } - - private fun fromUri(uri: String): JWSKeySelector { - return try { - JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(URL(uri)) <4> - } catch (ex: Exception) { - throw IllegalArgumentException(ex) - } - } -} ----- -==== -<1> A hypothetical source for tenant information -<2> A cache for `JWKKeySelector`s, keyed by tenant identifier -<3> Looking up the tenant is more secure than simply calculating the JWK Set endpoint on the fly - the lookup acts as a list of allowed tenants -<4> Create a `JWSKeySelector` via the types of keys that come back from the JWK Set endpoint - the lazy lookup here means that you don't need to configure all tenants at startup - -The above key selector is a composition of many key selectors. -It chooses which key selector to use based on the `iss` claim in the JWT. - -NOTE: To use this approach, make sure that the authorization server is configured to include the claim set as part of the token's signature. -Without this, you have no guarantee that the issuer hasn't been altered by a bad actor. - -Next, we can construct a `JWTProcessor`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) { - ConfigurableJWTProcessor jwtProcessor = - new DefaultJWTProcessor(); - jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector); - return jwtProcessor; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtProcessor(keySelector: JWTClaimsSetAwareJWSKeySelector): JWTProcessor { - val jwtProcessor = DefaultJWTProcessor() - jwtProcessor.jwtClaimsSetAwareJWSKeySelector = keySelector - return jwtProcessor -} ----- -==== - -As you are already seeing, the trade-off for moving tenant-awareness down to this level is more configuration. -We have just a bit more. - -Next, we still want to make sure you are validating the issuer. -But, since the issuer may be different per JWT, then you'll need a tenant-aware validator, too: - -==== -.Java -[source,java,role="primary"] ----- -@Component -public class TenantJwtIssuerValidator implements OAuth2TokenValidator { - private final TenantRepository tenants; - private final Map validators = new ConcurrentHashMap<>(); - - public TenantJwtIssuerValidator(TenantRepository tenants) { - this.tenants = tenants; - } - - @Override - public OAuth2TokenValidatorResult validate(Jwt token) { - return this.validators.computeIfAbsent(toTenant(token), this::fromTenant) - .validate(token); - } - - private String toTenant(Jwt jwt) { - return jwt.getIssuer(); - } - - private JwtIssuerValidator fromTenant(String tenant) { - return Optional.ofNullable(this.tenants.findById(tenant)) - .map(t -> t.getAttribute("issuer")) - .map(JwtIssuerValidator::new) - .orElseThrow(() -> new IllegalArgumentException("unknown tenant")); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Component -class TenantJwtIssuerValidator(tenants: TenantRepository) : OAuth2TokenValidator { - private val tenants: TenantRepository - private val validators: MutableMap = ConcurrentHashMap() - override fun validate(token: Jwt): OAuth2TokenValidatorResult { - return validators.computeIfAbsent(toTenant(token)) { tenant: String -> fromTenant(tenant) } - .validate(token) - } - - private fun toTenant(jwt: Jwt): String { - return jwt.issuer.toString() - } - - private fun fromTenant(tenant: String): JwtIssuerValidator { - return Optional.ofNullable(tenants.findById(tenant)) - .map({ t -> t.getAttribute("issuer") }) - .map({ JwtIssuerValidator() }) - .orElseThrow({ IllegalArgumentException("unknown tenant") }) - } - - init { - this.tenants = tenants - } -} ----- -==== - -Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our <>: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator jwtValidator) { - NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor); - OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<> - (JwtValidators.createDefault(), this.jwtValidator); - decoder.setJwtValidator(validator); - return decoder; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(jwtProcessor: JWTProcessor?, jwtValidator: OAuth2TokenValidator?): JwtDecoder { - val decoder = NimbusJwtDecoder(jwtProcessor) - val validator: OAuth2TokenValidator = DelegatingOAuth2TokenValidator(JwtValidators.createDefault(), jwtValidator) - decoder.setJwtValidator(validator) - return decoder -} ----- -==== - -We've finished talking about resolving the tenant. - -If you've chosen to resolve the tenant by something other than a JWT claim, then you'll need to make sure you address your downstream resource servers in the same way. -For example, if you are resolving it by subdomain, you may need to address the downstream resource server using the same subdomain. - -However, if you resolve it by a claim in the bearer token, read on to learn about <>. - -[[oauth2resourceserver-bearertoken-resolver]] -== Bearer Token Resolution - -By default, Resource Server looks for a bearer token in the `Authorization` header. -This, however, can be customized in a handful of ways. - -=== Reading the Bearer Token from a Custom Header - -For example, you may have a need to read the bearer token from a custom header. -To achieve this, you can expose a `DefaultBearerTokenResolver` as a bean, or wire an instance into the DSL, as you can see in the following example: - -.Custom Bearer Token Header -==== -.Java -[source,java,role="primary"] ----- -@Bean -BearerTokenResolver bearerTokenResolver() { - DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); - bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION); - return bearerTokenResolver; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun bearerTokenResolver(): BearerTokenResolver { - val bearerTokenResolver = DefaultBearerTokenResolver() - bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION) - return bearerTokenResolver -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - ----- -==== - -Or, in circumstances where a provider is using both a custom header and value, you can use `HeaderBearerTokenResolver` instead. - -=== Reading the Bearer Token from a Form Parameter - -Or, you may wish to read the token from a form parameter, which you can do by configuring the `DefaultBearerTokenResolver`, as you can see below: - -.Form Parameter Bearer Token -==== -.Java -[source,java,role="primary"] ----- -DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); -resolver.setAllowFormEncodedBodyParameter(true); -http - .oauth2ResourceServer(oauth2 -> oauth2 - .bearerTokenResolver(resolver) - ); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val resolver = DefaultBearerTokenResolver() -resolver.setAllowFormEncodedBodyParameter(true) -http { - oauth2ResourceServer { - bearerTokenResolver = resolver - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - ----- -==== - -== Bearer Token Propagation - -Now that you're resource server has validated the token, it might be handy to pass it to downstream services. -This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.html[ServletBearerExchangeFilterFunction]`, which you can see in the following example: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public WebClient rest() { - return WebClient.builder() - .filter(new ServletBearerExchangeFilterFunction()) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun rest(): WebClient { - return WebClient.builder() - .filter(ServletBearerExchangeFilterFunction()) - .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) - .block() ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -this.rest.get() - .uri("https://other-service.example.com/endpoint") - .retrieve() - .bodyToMono() - .block() ----- -==== - -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) - .block() ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -this.rest.get() - .uri("https://other-service.example.com/endpoint") - .headers{ headers -> headers.setBearerAuth(overridingToken)} - .retrieve() - .bodyToMono() - .block() ----- -==== - -In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain. - -[NOTE] -Unlike the {security-api-url}org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.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. - -=== `RestTemplate` support - -There is no `RestTemplate` equivalent for `ServletBearerExchangeFilterFunction` at the moment, but you can propagate the request's bearer token quite simply with your own interceptor: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -RestTemplate rest() { - RestTemplate rest = new RestTemplate(); - rest.getInterceptors().add((request, body, execution) -> { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null) { - return execution.execute(request, body); - } - - if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) { - return execution.execute(request, body); - } - - AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials(); - request.getHeaders().setBearerAuth(token.getTokenValue()); - return execution.execute(request, body); - }); - return rest; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun rest(): RestTemplate { - val rest = RestTemplate() - rest.interceptors.add(ClientHttpRequestInterceptor { request, body, execution -> - val authentication: Authentication? = SecurityContextHolder.getContext().authentication - if (authentication != null) { - execution.execute(request, body) - } - - if (authentication!!.credentials !is AbstractOAuth2Token) { - execution.execute(request, body) - } - - val token: AbstractOAuth2Token = authentication.credentials as AbstractOAuth2Token - request.headers.setBearerAuth(token.tokenValue) - execution.execute(request, body) - }) - return rest -} ----- -==== - - -[NOTE] -Unlike the {security-api-url}org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.html[OAuth 2.0 Authorized Client Manager], this filter interceptor makes no attempt to renew the token, should it be expired. -To obtain this level of support, please create an interceptor using the xref:servlet/oauth2/oauth2-client.adoc#oauth2client[OAuth 2.0 Authorized Client Manager]. - -[[oauth2resourceserver-bearertoken-failure]] -== Bearer Token Failure - -A bearer token may be invalid for a number of reasons. For example, the token may no longer be active. - -In these circumstances, Resource Server throws an `InvalidBearerTokenException`. -Like other exceptions, this results in an OAuth 2.0 Bearer Token error response: - -[source,http request] ----- -HTTP/1.1 401 Unauthorized -WWW-Authenticate: Bearer error_code="invalid_token", error_description="Unsupported algorithm of none", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1" ----- - -Additionally, it is published as an `AuthenticationFailureBadCredentialsEvent`, which you can xref:servlet/authentication/events.adoc#servlet-events[listen for in your application] like so: - -==== -.Java -[source,java,role="primary"] ----- -@Component -public class FailureEvents { - @EventListener - public void onFailure(AuthenticationFailureBadCredentialsEvent badCredentials) { - if (badCredentials.getAuthentication() instanceof BearerTokenAuthenticationToken) { - // ... handle - } - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Component -class FailureEvents { - @EventListener - fun onFailure(badCredentials: AuthenticationFailureBadCredentialsEvent) { - if (badCredentials.authentication is BearerTokenAuthenticationToken) { - // ... handle - } - } -} ----- -==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc new file mode 100644 index 0000000000..9c35a4b87d --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc @@ -0,0 +1,290 @@ += OAuth 2.0 Bearer Tokens + +[[oauth2resourceserver-bearertoken-resolver]] +== Bearer Token Resolution + +By default, Resource Server looks for a bearer token in the `Authorization` header. +This, however, can be customized in a handful of ways. + +=== Reading the Bearer Token from a Custom Header + +For example, you may have a need to read the bearer token from a custom header. +To achieve this, you can expose a `DefaultBearerTokenResolver` as a bean, or wire an instance into the DSL, as you can see in the following example: + +.Custom Bearer Token Header +==== +.Java +[source,java,role="primary"] +---- +@Bean +BearerTokenResolver bearerTokenResolver() { + DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION); + return bearerTokenResolver; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun bearerTokenResolver(): BearerTokenResolver { + val bearerTokenResolver = DefaultBearerTokenResolver() + bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION) + return bearerTokenResolver +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + +---- +==== + +Or, in circumstances where a provider is using both a custom header and value, you can use `HeaderBearerTokenResolver` instead. + +=== Reading the Bearer Token from a Form Parameter + +Or, you may wish to read the token from a form parameter, which you can do by configuring the `DefaultBearerTokenResolver`, as you can see below: + +.Form Parameter Bearer Token +==== +.Java +[source,java,role="primary"] +---- +DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); +resolver.setAllowFormEncodedBodyParameter(true); +http + .oauth2ResourceServer(oauth2 -> oauth2 + .bearerTokenResolver(resolver) + ); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val resolver = DefaultBearerTokenResolver() +resolver.setAllowFormEncodedBodyParameter(true) +http { + oauth2ResourceServer { + bearerTokenResolver = resolver + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + +---- +==== + +== Bearer Token Propagation + +Now that you're resource server has validated the token, it might be handy to pass it to downstream services. +This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.html[ServletBearerExchangeFilterFunction]`, which you can see in the following example: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public WebClient rest() { + return WebClient.builder() + .filter(new ServletBearerExchangeFilterFunction()) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun rest(): WebClient { + return WebClient.builder() + .filter(ServletBearerExchangeFilterFunction()) + .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) + .block() +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +this.rest.get() + .uri("https://other-service.example.com/endpoint") + .retrieve() + .bodyToMono() + .block() +---- +==== + +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) + .block() +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +this.rest.get() + .uri("https://other-service.example.com/endpoint") + .headers{ headers -> headers.setBearerAuth(overridingToken)} + .retrieve() + .bodyToMono() + .block() +---- +==== + +In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain. + +[NOTE] +Unlike the {security-api-url}org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.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. + +=== `RestTemplate` support + +There is no `RestTemplate` equivalent for `ServletBearerExchangeFilterFunction` at the moment, but you can propagate the request's bearer token quite simply with your own interceptor: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +RestTemplate rest() { + RestTemplate rest = new RestTemplate(); + rest.getInterceptors().add((request, body, execution) -> { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return execution.execute(request, body); + } + + if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) { + return execution.execute(request, body); + } + + AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials(); + request.getHeaders().setBearerAuth(token.getTokenValue()); + return execution.execute(request, body); + }); + return rest; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun rest(): RestTemplate { + val rest = RestTemplate() + rest.interceptors.add(ClientHttpRequestInterceptor { request, body, execution -> + val authentication: Authentication? = SecurityContextHolder.getContext().authentication + if (authentication != null) { + execution.execute(request, body) + } + + if (authentication!!.credentials !is AbstractOAuth2Token) { + execution.execute(request, body) + } + + val token: AbstractOAuth2Token = authentication.credentials as AbstractOAuth2Token + request.headers.setBearerAuth(token.tokenValue) + execution.execute(request, body) + }) + return rest +} +---- +==== + + +[NOTE] +Unlike the {security-api-url}org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.html[OAuth 2.0 Authorized Client Manager], this filter interceptor makes no attempt to renew the token, should it be expired. +To obtain this level of support, please create an interceptor using the xref:servlet/oauth2/oauth2-client.adoc#oauth2client[OAuth 2.0 Authorized Client Manager]. + +[[oauth2resourceserver-bearertoken-failure]] +== Bearer Token Failure + +A bearer token may be invalid for a number of reasons. For example, the token may no longer be active. + +In these circumstances, Resource Server throws an `InvalidBearerTokenException`. +Like other exceptions, this results in an OAuth 2.0 Bearer Token error response: + +[source,http request] +---- +HTTP/1.1 401 Unauthorized +WWW-Authenticate: Bearer error_code="invalid_token", error_description="Unsupported algorithm of none", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1" +---- + +Additionally, it is published as an `AuthenticationFailureBadCredentialsEvent`, which you can xref:servlet/authentication/events.adoc#servlet-events[listen for in your application] like so: + +==== +.Java +[source,java,role="primary"] +---- +@Component +public class FailureEvents { + @EventListener + public void onFailure(AuthenticationFailureBadCredentialsEvent badCredentials) { + if (badCredentials.getAuthentication() instanceof BearerTokenAuthenticationToken) { + // ... handle + } + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Component +class FailureEvents { + @EventListener + fun onFailure(badCredentials: AuthenticationFailureBadCredentialsEvent) { + if (badCredentials.authentication is BearerTokenAuthenticationToken) { + // ... handle + } + } +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc new file mode 100644 index 0000000000..1633c5bacf --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc @@ -0,0 +1,58 @@ +[[oauth2resourceserver]] += OAuth 2.0 Resource Server +:figures: servlet/oauth2 + +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. + +This section provides details on how Spring Security provides support for OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]. + +[NOTE] +==== +Working samples for both {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/jwe[JWTs] and {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security Samples repository]. +==== + +Let's take a look at how Bearer Token Authentication works within Spring Security. +First, we see that, like xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client. + +.Sending WWW-Authenticate Header +image::{figures}/bearerauthenticationentrypoint.png[] + +The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram. + +image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized. + +image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. + +image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__. +The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`] which sends a WWW-Authenticate header. +The `RequestCache` is typically a `NullRequestCache` that does not save the request since the client is capable of replaying the requests it originally requested. + +When a client receives the `WWW-Authenticate: Bearer` header, it knows it should retry with a bearer token. +Below is the flow for the bearer token being processed. + +[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]] +.Authenticating Bearer Token +image::{figures}/bearertokenauthenticationfilter.png[] + +The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram. + +image:{icondir}/number_1.png[] When the user submits their bearer token, the `BearerTokenAuthenticationFilter` creates a `BearerTokenAuthenticationToken` which is a type of xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] by extracting the token from the `HttpServletRequest`. + +image:{icondir}/number_2.png[] Next, the `HttpServletRequest` is passed to the `AuthenticationManagerResolver`, which selects the `AuthenticationManager`. The `BearerTokenAuthenticationToken` is passed into the `AuthenticationManager` to be authenticated. +The details of what `AuthenticationManager` looks like depends on whether you're configured for xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-minimalconfiguration[JWT] or xref:servlet/oauth2/resource-server/opaque-token.adoc#oauth2resourceserver-opaque-minimalconfiguration[opaque token]. + +image:{icondir}/number_3.png[] If authentication fails, then __Failure__ + +* The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder] is cleared out. +* The `AuthenticationEntryPoint` is invoked to trigger the WWW-Authenticate header to be sent again. + +image:{icondir}/number_4.png[] If authentication is successful, then __Success__. + +* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. +* The `BearerTokenAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc new file mode 100644 index 0000000000..64ebfda12f --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc @@ -0,0 +1,1380 @@ += OAuth 2.0 Resource Server JWT +:figures: servlet/oauth2 + +[[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. + +[[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. Query the Provider Configuration or Authorization Server Metadata endpoint for the `jwks_url` property +2. Query the `jwks_url` endpoint for supported algorithms +3. Configure the validation strategy to query `jwks_url` for valid public keys of the algorithms found +4. 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 JWT +2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `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 JWTs. + +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: + +* <> +* <> +* <> + +[[oauth2resourceserver-jwt-architecture]] +== How JWT Authentication Works + +Next, let's see the architectural components that Spring Security uses to support https://tools.ietf.org/html/rfc7519[JWT] Authentication in servlet-based applications, like the one we just saw. + +{security-api-url}org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.html[`JwtAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> and <> to authenticate a JWT. + +Let's take a look at how `JwtAuthenticationProvider` works within Spring Security. +The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <> works. + +.`JwtAuthenticationProvider` Usage +image::{figures}/jwtauthenticationprovider.png[] + +image:{icondir}/number_1.png[] The authentication `Filter` from <> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. + +image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `JwtAuthenticationProvider`. + +[[oauth2resourceserver-jwt-architecture-jwtdecoder]] +image:{icondir}/number_3.png[] `JwtAuthenticationProvider` decodes, verifies, and validates the `Jwt` using a <>. + +[[oauth2resourceserver-jwt-architecture-jwtauthenticationconverter]] +image:{icondir}/number_4.png[] `JwtAuthenticationProvider` then uses the <> to convert the `Jwt` into a `Collection` of granted authorities. + +image:{icondir}/number_5.png[] When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `JwtAuthenticationToken` and has a principal that is the `Jwt` returned by the configured `JwtDecoder`. +Ultimately, the returned `JwtAuthenticationToken` will be set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] by the authentication `Filter`. + +[[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 <>. + +[[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 `WebSecurityConfigurerAdapter` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `WebSecurityConfigurerAdapter` looks like: + +.Default JWT Configuration +==== +.Java +[source,java,role="primary"] +---- +protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { } + } + } +} +---- +==== + +If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one. + +Replacing this is as simple as exposing the bean within the application: + +.Custom JWT Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(myConverter()) + ) + ); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/messages/**", hasAuthority("SCOPE_message:read")) + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { + jwtAuthenticationConverter = myConverter() + } + } + } + } +} +---- +==== + +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. + +[[oauth2resourceserver-jwt-decoder]] +For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which <>: + +.JWT Decoder +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtDecoder jwtDecoder() { + return JwtDecoders.fromIssuerLocation(issuerUri); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + return JwtDecoders.fromIssuerLocation(issuerUri) +} +---- +==== + +[NOTE] +Calling `{security-api-url}org/springframework/security/oauth2/jwt/JwtDecoders.html#fromIssuerLocation-java.lang.String-[JwtDecoders#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 `JwtDecoder` bean, then Spring Boot will expose the above default one. + +And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`. + +Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a `JwtDecoder` can be specified in XML. + +The filter chain is specified like so: + +.Default JWT Configuration +==== +.Xml +[source,xml,role="primary"] +---- + + + + + + +---- +==== + +And the `JwtDecoder` like so: + +.JWT Decoder +==== +.Xml +[source,xml,role="primary"] +---- + + + +---- +==== + +[[oauth2resourceserver-jwt-jwkseturi-dsl]] +=== Using `jwkSetUri()` + +An authorization server's JWK Set Uri can be configured <> or it can be supplied in the DSL: + +.JWK Set Uri Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwkSetUri("https://idp.example.com/.well-known/jwks.json") + ) + ); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { + jwkSetUri = "https://idp.example.com/.well-known/jwks.json" + } + } + } + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + +---- +==== + +Using `jwkSetUri()` takes precedence over any configuration property. + +[[oauth2resourceserver-jwt-decoder-dsl]] +=== Using `decoder()` + +More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of <>: + +.JWT Decoder Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .decoder(myCustomDecoder()) + ) + ); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class DirectlyConfiguredJwtDecoder : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { + jwtDecoder = myCustomDecoder() + } + } + } + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + +---- +==== + +This is handy when deeper configuration, like <>, <>, or <>, is necessary. + +[[oauth2resourceserver-jwt-decoder-bean]] +=== Exposing a `JwtDecoder` `@Bean` + +Or, exposing a <> `@Bean` has the same effect as `decoder()`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() +} +---- +==== + +[[oauth2resourceserver-jwt-decoder-algorithm]] +== Configuring Trusted Algorithms + +By default, `NimbusJwtDecoder`, and hence Resource Server, will only trust and verify tokens using `RS256`. + +You can customize this via <>, <>, or from the <>. + +[[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 +---- + +[[oauth2resourceserver-jwt-decoder-builder]] +=== Using a Builder + +For greater power, though, we can use a builder that ships with `NimbusJwtDecoder`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).build() +} +---- +==== + +Calling `jwsAlgorithm` more than once will configure `NimbusJwtDecoder` to trust more than one algorithm, like so: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build() +} +---- +==== + +Or, you can call `jwsAlgorithms`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithms(algorithms -> { + algorithms.add(RS512); + algorithms.add(ES512); + }).build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithms { + it.add(RS512) + it.add(ES512) + }.build() +} +---- +==== + +[[oauth2resourceserver-jwt-decoder-jwk-response]] +=== From JWK Set response + +Since Spring Security's JWT support is based off of Nimbus, you can use all it's great features as well. + +For example, Nimbus has a `JWSKeySelector` implementation that will select the set of algorithms based on the JWK Set URI response. +You can use it to generate a `NimbusJwtDecoder` like so: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtDecoder jwtDecoder() { + // makes a request to the JWK Set endpoint + JWSKeySelector jwsKeySelector = + JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl); + + DefaultJWTProcessor jwtProcessor = + new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + + return new NimbusJwtDecoder(jwtProcessor); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + // makes a request to the JWK Set endpoint + val jwsKeySelector: JWSKeySelector = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl) + val jwtProcessor: DefaultJWTProcessor = DefaultJWTProcessor() + jwtProcessor.jwsKeySelector = jwsKeySelector + return NimbusJwtDecoder(jwtProcessor) +} +---- +==== + +[[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 <> or by <>. + +[[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`: + +==== +.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 -> + beanFactory.getBean() + .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 +---- +==== + +[[oauth2resourceserver-jwt-decoder-public-key-builder]] +=== Using a Builder + +To wire an `RSAPublicKey` directly, you can simply use the appropriate `NimbusJwtDecoder` builder, like so: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withPublicKey(this.key).build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + return NimbusJwtDecoder.withPublicKey(this.key).build() +} +---- +==== + +[[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 `NimbusJwtDecoder` builder, like so: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withSecretKey(this.key).build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + return NimbusJwtDecoder.withSecretKey(key).build() +} +---- +==== + +[[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: + +.Authorization Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/contacts/**", hasAuthority("SCOPE_contacts")) + authorize("/messages/**", hasAuthority("SCOPE_messages")) + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { } + } + } + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + +---- +==== + +Or similarly with method security: + +==== +.Java +[source,java,role="primary"] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +public List getMessages(...) {} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +fun getMessages(): List { } +---- +==== + +[[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, Spring Security ships with `JwtAuthenticationConverter`, which is responsible for <>. +By default, Spring Security will wire the `JwtAuthenticationProvider` with a default instance of `JwtAuthenticationConverter`. + +As part of configuring a `JwtAuthenticationConverter`, you can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities. + +Let's say that that your authorization server communicates authorities in a custom claim called `authorities`. +In that case, you can configure the claim that <> should inspect, like so: + +.Authorities Claim Configuration +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtAuthenticationConverter(): JwtAuthenticationConverter { + val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter() + grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities") + + val jwtAuthenticationConverter = JwtAuthenticationConverter() + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter) + return jwtAuthenticationConverter +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + + + + + + + + + +---- +==== + +You can also configure the authority prefix to be different as well. +Instead of prefixing each authority with `SCOPE_`, you can change it to `ROLE_` like so: + +.Authorities Prefix Configuration +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtAuthenticationConverter(): JwtAuthenticationConverter { + val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter() + grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_") + + val jwtAuthenticationConverter = JwtAuthenticationConverter() + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter) + return jwtAuthenticationConverter +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + + + + + + + + + +---- +==== + +Or, you can remove the prefix altogether by calling `JwtGrantedAuthoritiesConverter#setAuthorityPrefix("")`. + +For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter`: + +==== +.Java +[source,java,role="primary"] +---- +static class CustomAuthenticationConverter implements Converter { + public AbstractAuthenticationToken convert(Jwt jwt) { + return new CustomAuthenticationToken(jwt); + } +} + +// ... + +@EnableWebSecurity +public class CustomAuthenticationConverterConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(new CustomAuthenticationConverter()) + ) + ); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +internal class CustomAuthenticationConverter : Converter { + override fun convert(jwt: Jwt): AbstractAuthenticationToken { + return CustomAuthenticationToken(jwt) + } +} + +// ... + +@EnableWebSecurity +class CustomAuthenticationConverterConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { + jwtAuthenticationConverter = CustomAuthenticationConverter() + } + } + } + } +} +---- +==== + +[[oauth2resourceserver-jwt-validation]] +== Configuring Validation + +Using <>, 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. + +[[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 +JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) + JwtDecoders.fromIssuerLocation(issuerUri); + + OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator<>( + new JwtTimestampValidator(Duration.ofSeconds(60)), + new JwtIssuerValidator(issuerUri)); + + jwtDecoder.setJwtValidator(withClockSkew); + + return jwtDecoder; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder + + val withClockSkew: OAuth2TokenValidator = DelegatingOAuth2TokenValidator( + JwtTimestampValidator(Duration.ofSeconds(60)), + JwtIssuerValidator(issuerUri)) + + jwtDecoder.setJwtValidator(withClockSkew) + + return jwtDecoder +} +---- +==== + +[NOTE] +By default, Resource Server configures a clock skew of 60 seconds. + +[[oauth2resourceserver-jwt-validation-custom]] +=== Configuring a Custom Validator + +Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API: + +==== +.Java +[source,java,role="primary"] +---- +OAuth2TokenValidator audienceValidator() { + return new JwtClaimValidator>(AUD, aud -> aud.contains("messaging")); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +fun audienceValidator(): OAuth2TokenValidator { + return JwtClaimValidator>(AUD) { aud -> aud.contains("messaging") } +} +---- +==== + +Or, for more control you can implement your own `OAuth2TokenValidator`: + +==== +.Java +[source,java,role="primary"] +---- +static class AudienceValidator implements OAuth2TokenValidator { + OAuth2Error error = new OAuth2Error("custom_code", "Custom error message", null); + + @Override + public OAuth2TokenValidatorResult validate(Jwt jwt) { + if (jwt.getAudience().contains("messaging")) { + return OAuth2TokenValidatorResult.success(); + } else { + return OAuth2TokenValidatorResult.failure(error); + } + } +} + +// ... + +OAuth2TokenValidator audienceValidator() { + return new AudienceValidator(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +internal class AudienceValidator : OAuth2TokenValidator { + var error: OAuth2Error = OAuth2Error("custom_code", "Custom error message", null) + + override fun validate(jwt: Jwt): OAuth2TokenValidatorResult { + return if (jwt.audience.contains("messaging")) { + OAuth2TokenValidatorResult.success() + } else { + OAuth2TokenValidatorResult.failure(error) + } + } +} + +// ... + +fun audienceValidator(): OAuth2TokenValidator { + return AudienceValidator() +} +---- +==== + +Then, to add into a resource server, it's a matter of specifying the <> instance: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) + JwtDecoders.fromIssuerLocation(issuerUri); + + OAuth2TokenValidator audienceValidator = audienceValidator(); + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); + OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); + + jwtDecoder.setJwtValidator(withAudience); + + return jwtDecoder; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder + + val audienceValidator = audienceValidator() + val withIssuer: OAuth2TokenValidator = JwtValidators.createDefaultWithIssuer(issuerUri) + val withAudience: OAuth2TokenValidator = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator) + + jwtDecoder.setJwtValidator(withAudience) + + return jwtDecoder +} +---- +==== + +[[oauth2resourceserver-jwt-claimsetmapping]] +== Configuring Claim Set Mapping + +Spring Security uses the https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home[Nimbus] library for parsing JWTs and validating their signatures. +Consequently, Spring Security is subject to Nimbus's interpretation of each field value and how to coerce each into a Java type. + +For example, because Nimbus remains Java 7 compatible, it doesn't use `Instant` to represent timestamp fields. + +And it's entirely possible to use a different library or for JWT processing, which may make its own coercion decisions that need adjustment. + +Or, quite simply, a resource server may want to add or remove claims from a JWT for domain-specific reasons. + +For these purposes, Resource Server supports mapping the JWT claim set with `MappedJwtClaimSetConverter`. + +[[oauth2resourceserver-jwt-claimsetmapping-singleclaim]] +=== Customizing the Conversion of a Single Claim + +By default, `MappedJwtClaimSetConverter` will attempt to coerce claims into the following types: + +|============ +| Claim | Java Type +| `aud` | `Collection` +| `exp` | `Instant` +| `iat` | `Instant` +| `iss` | `String` +| `jti` | `String` +| `nbf` | `Instant` +| `sub` | `String` +|============ + +An individual claim's conversion strategy can be configured using `MappedJwtClaimSetConverter.withDefaults`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); + + MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter + .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub)); + jwtDecoder.setClaimSetConverter(converter); + + return jwtDecoder; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + val jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() + + val converter = MappedJwtClaimSetConverter + .withDefaults(mapOf("sub" to this::lookupUserIdBySub)) + jwtDecoder.setClaimSetConverter(converter) + + return jwtDecoder +} +---- +==== +This will keep all the defaults, except it will override the default claim converter for `sub`. + +[[oauth2resourceserver-jwt-claimsetmapping-add]] +=== Adding a Claim + +`MappedJwtClaimSetConverter` can also be used to add a custom claim, for example, to adapt to an existing system: + +==== +.Java +[source,java,role="primary"] +---- +MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value")); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter { "value" })) +---- +==== + +[[oauth2resourceserver-jwt-claimsetmapping-remove]] +=== Removing a Claim + +And removing a claim is also simple, using the same API: + +==== +.Java +[source,java,role="primary"] +---- +MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null)); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter { null })) +---- +==== + +[[oauth2resourceserver-jwt-claimsetmapping-rename]] +=== Renaming a Claim + +In more sophisticated scenarios, like consulting multiple claims at once or renaming a claim, Resource Server accepts any class that implements `Converter, Map>`: + +==== +.Java +[source,java,role="primary"] +---- +public class UsernameSubClaimAdapter implements Converter, Map> { + private final MappedJwtClaimSetConverter delegate = + MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); + + public Map convert(Map claims) { + Map convertedClaims = this.delegate.convert(claims); + + String username = (String) convertedClaims.get("user_name"); + convertedClaims.put("sub", username); + + return convertedClaims; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class UsernameSubClaimAdapter : Converter, Map> { + private val delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()) + override fun convert(claims: Map): Map { + val convertedClaims = delegate.convert(claims) + val username = convertedClaims["user_name"] as String + convertedClaims["sub"] = username + return convertedClaims + } +} +---- +==== + +And then, the instance can be supplied like normal: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); + jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter()); + return jwtDecoder; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): JwtDecoder { + val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() + jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter()) + return jwtDecoder +} +---- +==== + +[[oauth2resourceserver-jwt-timeouts]] +== Configuring Timeouts + +By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server. + +This may be too short in some scenarios. +Further, it doesn't take into account more sophisticated patterns like back-off and discovery. + +To adjust the way in which Resource Server connects to the authorization server, `NimbusJwtDecoder` accepts an instance of `RestOperations`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { + RestOperations rest = builder + .setConnectTimeout(Duration.ofSeconds(60)) + .setReadTimeout(Duration.ofSeconds(60)) + .build(); + + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build(); + return jwtDecoder; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder { + val rest: RestOperations = builder + .setConnectTimeout(Duration.ofSeconds(60)) + .setReadTimeout(Duration.ofSeconds(60)) + .build() + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build() +} +---- +==== + +Also by default, Resource Server caches in-memory the authorization server's JWK set for 5 minutes, which you may want to adjust. +Further, it doesn't take into account more sophisticated caching patterns like eviction or using a shared cache. + +To adjust the way in which Resource Server caches the JWK set, `NimbusJwtDecoder` accepts an instance of `Cache`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtDecoder jwtDecoder(CacheManager cacheManager) { + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) + .cache(cacheManager.getCache("jwks")) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(cacheManager: CacheManager): JwtDecoder { + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) + .cache(cacheManager.getCache("jwks")) + .build() +} +---- +==== + +When given a `Cache`, Resource Server will use the JWK Set Uri as the key and the JWK Set JSON as the value. + +NOTE: Spring isn't a cache provider, so you'll need to make sure to include the appropriate dependencies, like `spring-boot-starter-cache` and your favorite caching provider. + +NOTE: Whether it's socket or cache timeouts, you may instead want to work with Nimbus directly. +To do so, remember that `NimbusJwtDecoder` ships with a constructor that takes Nimbus's `JWTProcessor`. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc new file mode 100644 index 0000000000..07f804d5cc --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc @@ -0,0 +1,445 @@ += OAuth 2.0 Resource Server Multitenancy + +[[oauth2reourceserver-opaqueandjwt]] +== Supporting both JWT and Opaque Token + +In some cases, you may have a need to access both kinds of tokens. +For example, you may support more than one tenant where one tenant issues JWTs and the other issues opaque tokens. + +If this decision must be made at request-time, then you can use an `AuthenticationManagerResolver` to achieve it, like so: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +AuthenticationManagerResolver tokenAuthenticationManagerResolver + (JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) { + AuthenticationManager jwt = new ProviderManager(new JwtAuthenticationProvider(jwtDecoder)); + AuthenticationManager opaqueToken = new ProviderManager( + new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)); + return (request) -> useJwt(request) ? jwt : opaqueToken; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun tokenAuthenticationManagerResolver + (jwtDecoder: JwtDecoder, opaqueTokenIntrospector: OpaqueTokenIntrospector): + AuthenticationManagerResolver { + val jwt = ProviderManager(JwtAuthenticationProvider(jwtDecoder)) + val opaqueToken = ProviderManager(OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)); + + return AuthenticationManagerResolver { request -> + if (useJwt(request)) { + jwt + } else { + opaqueToken + } + } +} +---- +==== + +NOTE: The implementation of `useJwt(HttpServletRequest)` will likely depend on custom request material like the path. + +And then specify this `AuthenticationManagerResolver` in the DSL: + +.Authentication Manager Resolver +==== +.Java +[source,java,role="primary"] +---- +http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .authenticationManagerResolver(this.tokenAuthenticationManagerResolver) + ); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + authenticationManagerResolver = tokenAuthenticationManagerResolver() + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + +---- +==== + +[[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 `JwtIssuerAuthenticationManagerResolver`, like so: + +.Multitenancy Tenant by JWT Claim +==== +.Java +[source,java,role="primary"] +---- +JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver + ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); + +http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .authenticationManagerResolver(authenticationManagerResolver) + ); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val customAuthenticationManagerResolver = JwtIssuerAuthenticationManagerResolver + ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo") +http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + authenticationManagerResolver = customAuthenticationManagerResolver + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + + https://idp.example.org/issuerOne + https://idp.example.org/issuerTwo + + + +---- +==== + +This is nice because the issuer endpoints are loaded lazily. +In fact, the corresponding `JwtAuthenticationProvider` 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 `JwtIssuerAuthenticationManagerResolver` with a repository of `AuthenticationManager` instances, which you can edit at runtime, like so: + +==== +.Java +[source,java,role="primary"] +---- +private void addManager(Map authenticationManagers, String issuer) { + JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider + (JwtDecoders.fromIssuerLocation(issuer)); + authenticationManagers.put(issuer, authenticationProvider::authenticate); +} + +// ... + +JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = + new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get); + +http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .authenticationManagerResolver(authenticationManagerResolver) + ); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +private fun addManager(authenticationManagers: MutableMap, issuer: String) { + val authenticationProvider = JwtAuthenticationProvider(JwtDecoders.fromIssuerLocation(issuer)) + authenticationManagers[issuer] = AuthenticationManager { + authentication: Authentication? -> authenticationProvider.authenticate(authentication) + } +} + +// ... + +val customAuthenticationManagerResolver: JwtIssuerAuthenticationManagerResolver = + JwtIssuerAuthenticationManagerResolver(authenticationManagers::get) +http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + authenticationManagerResolver = customAuthenticationManagerResolver + } +} +---- +==== + +In this case, you construct `JwtIssuerAuthenticationManagerResolver` with a strategy for obtaining the `AuthenticationManager` 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 `AuthenticationManager` from it. +The issuer should be one that the code can verify from a trusted source like a list of allowed issuers. + +==== Parsing the Claim Only Once + +You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-architecture-jwtdecoder[`JwtDecoder`] later on in the request. + +This extra parsing can be alleviated by configuring the xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-architecture-jwtdecoder[`JwtDecoder`] directly with a `JWTClaimsSetAwareJWSKeySelector` from Nimbus: + +==== +.Java +[source,java,role="primary"] +---- +@Component +public class TenantJWSKeySelector + implements JWTClaimsSetAwareJWSKeySelector { + + private final TenantRepository tenants; <1> + private final Map> selectors = new ConcurrentHashMap<>(); <2> + + public TenantJWSKeySelector(TenantRepository tenants) { + this.tenants = tenants; + } + + @Override + public List selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext) + throws KeySourceException { + return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant) + .selectJWSKeys(jwsHeader, securityContext); + } + + private String toTenant(JWTClaimsSet claimSet) { + return (String) claimSet.getClaim("iss"); + } + + private JWSKeySelector fromTenant(String tenant) { + return Optional.ofNullable(this.tenantRepository.findById(tenant)) <3> + .map(t -> t.getAttrbute("jwks_uri")) + .map(this::fromUri) + .orElseThrow(() -> new IllegalArgumentException("unknown tenant")); + } + + private JWSKeySelector fromUri(String uri) { + try { + return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); <4> + } catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Component +class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimsSetAwareJWSKeySelector { + private val tenants: TenantRepository <1> + private val selectors: MutableMap> = ConcurrentHashMap() <2> + + init { + this.tenants = tenants + } + + fun selectKeys(jwsHeader: JWSHeader?, jwtClaimsSet: JWTClaimsSet, securityContext: SecurityContext): List { + return selectors.computeIfAbsent(toTenant(jwtClaimsSet)) { tenant: String -> fromTenant(tenant) } + .selectJWSKeys(jwsHeader, securityContext) + } + + private fun toTenant(claimSet: JWTClaimsSet): String { + return claimSet.getClaim("iss") as String + } + + private fun fromTenant(tenant: String): JWSKeySelector { + return Optional.ofNullable(this.tenants.findById(tenant)) <3> + .map { t -> t.getAttrbute("jwks_uri") } + .map { uri: String -> fromUri(uri) } + .orElseThrow { IllegalArgumentException("unknown tenant") } + } + + private fun fromUri(uri: String): JWSKeySelector { + return try { + JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(URL(uri)) <4> + } catch (ex: Exception) { + throw IllegalArgumentException(ex) + } + } +} +---- +==== +<1> A hypothetical source for tenant information +<2> A cache for `JWKKeySelector`s, keyed by tenant identifier +<3> Looking up the tenant is more secure than simply calculating the JWK Set endpoint on the fly - the lookup acts as a list of allowed tenants +<4> Create a `JWSKeySelector` via the types of keys that come back from the JWK Set endpoint - the lazy lookup here means that you don't need to configure all tenants at startup + +The above key selector is a composition of many key selectors. +It chooses which key selector to use based on the `iss` claim in the JWT. + +NOTE: To use this approach, make sure that the authorization server is configured to include the claim set as part of the token's signature. +Without this, you have no guarantee that the issuer hasn't been altered by a bad actor. + +Next, we can construct a `JWTProcessor`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) { + ConfigurableJWTProcessor jwtProcessor = + new DefaultJWTProcessor(); + jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector); + return jwtProcessor; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtProcessor(keySelector: JWTClaimsSetAwareJWSKeySelector): JWTProcessor { + val jwtProcessor = DefaultJWTProcessor() + jwtProcessor.jwtClaimsSetAwareJWSKeySelector = keySelector + return jwtProcessor +} +---- +==== + +As you are already seeing, the trade-off for moving tenant-awareness down to this level is more configuration. +We have just a bit more. + +Next, we still want to make sure you are validating the issuer. +But, since the issuer may be different per JWT, then you'll need a tenant-aware validator, too: + +==== +.Java +[source,java,role="primary"] +---- +@Component +public class TenantJwtIssuerValidator implements OAuth2TokenValidator { + private final TenantRepository tenants; + private final Map validators = new ConcurrentHashMap<>(); + + public TenantJwtIssuerValidator(TenantRepository tenants) { + this.tenants = tenants; + } + + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + return this.validators.computeIfAbsent(toTenant(token), this::fromTenant) + .validate(token); + } + + private String toTenant(Jwt jwt) { + return jwt.getIssuer(); + } + + private JwtIssuerValidator fromTenant(String tenant) { + return Optional.ofNullable(this.tenants.findById(tenant)) + .map(t -> t.getAttribute("issuer")) + .map(JwtIssuerValidator::new) + .orElseThrow(() -> new IllegalArgumentException("unknown tenant")); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Component +class TenantJwtIssuerValidator(tenants: TenantRepository) : OAuth2TokenValidator { + private val tenants: TenantRepository + private val validators: MutableMap = ConcurrentHashMap() + override fun validate(token: Jwt): OAuth2TokenValidatorResult { + return validators.computeIfAbsent(toTenant(token)) { tenant: String -> fromTenant(tenant) } + .validate(token) + } + + private fun toTenant(jwt: Jwt): String { + return jwt.issuer.toString() + } + + private fun fromTenant(tenant: String): JwtIssuerValidator { + return Optional.ofNullable(tenants.findById(tenant)) + .map({ t -> t.getAttribute("issuer") }) + .map({ JwtIssuerValidator() }) + .orElseThrow({ IllegalArgumentException("unknown tenant") }) + } + + init { + this.tenants = tenants + } +} +---- +==== + +Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-architecture-jwtdecoder[`JwtDecoder`]: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator jwtValidator) { + NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor); + OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<> + (JwtValidators.createDefault(), this.jwtValidator); + decoder.setJwtValidator(validator); + return decoder; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(jwtProcessor: JWTProcessor?, jwtValidator: OAuth2TokenValidator?): JwtDecoder { + val decoder = NimbusJwtDecoder(jwtProcessor) + val validator: OAuth2TokenValidator = DelegatingOAuth2TokenValidator(JwtValidators.createDefault(), jwtValidator) + decoder.setJwtValidator(validator) + return decoder +} +---- +==== + +We've finished talking about resolving the tenant. + +If you've chosen to resolve the tenant by something other than a JWT claim, then you'll need to make sure you address your downstream resource servers in the same way. +For example, if you are resolving it by subdomain, you may need to address the downstream resource server using the same subdomain. + +However, if you resolve it by a claim in the bearer token, read on to learn about xref:servlet/oauth2/resource-server/bearer-tokens.adoc#oauth2resourceserver-bearertoken-resolver[Spring Security's support for bearer token propagation]. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc new file mode 100644 index 0000000000..4e0c618599 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc @@ -0,0 +1,891 @@ += OAuth 2.0 Resource Server Opaque Token +:figures: servlet/oauth2 + +[[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 <> is provided, the Resource Server will fallback to NimbusOpaqueTokenIntrospector. +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`. + +[[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. + +[[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: + +* <> +* <> +* <> +* <> + +[[oauth2resourceserver-opaque-architecture]] +== How Opaque Token Authentication Works + +Next, let's see the architectural components that Spring Security uses to support https://tools.ietf.org/html/rfc7662[opaque token] Authentication in servlet-based applications, like the one we just saw. + +{security-api-url}org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.html[`OpaqueTokenAuthenticationProvider`] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> to authenticate an opaque token. + +Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring Security. +The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <> works. + +.`OpaqueTokenAuthenticationProvider` Usage +image::{figures}/opaquetokenauthenticationprovider.png[] + +image:{icondir}/number_1.png[] The authentication `Filter` from <> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. + +image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `OpaqueTokenAuthenticationProvider`. + +[[oauth2resourceserver-opaque-architecture-introspector]] +image:{icondir}/number_3.png[] `OpaqueTokenAuthenticationProvider` introspects the opaque token and adds granted authorities using an <>. +When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `BearerTokenAuthentication` and has a principal that is the `OAuth2AuthenticatedPrincipal` returned by the configured <>. +Ultimately, the returned `BearerTokenAuthentication` will be set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] by the authentication `Filter`. + +[[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 `@EnableWebMvc` in your configuration: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/foo") +public String foo(BearerTokenAuthentication authentication) { + return authentication.getTokenAttributes().get("sub") + " is the subject"; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/foo") +fun foo(authentication: BearerTokenAuthentication): String { + return 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 String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) { + return principal.getAttribute("sub") + " is the subject"; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/foo") +fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): String { + return principal.getAttribute("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 `@EnableGlobalMethodSecurity` so that you can use `@PreAuthorize` annotations, you can do: + +==== +.Java +[source,java,role="primary"] +---- +@PreAuthorize("principal?.attributes['sub'] == 'foo'") +public String forFoosEyesOnly() { + return "foo"; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@PreAuthorize("principal?.attributes['sub'] == 'foo'") +fun forFoosEyesOnly(): String { + return "foo" +} +---- +==== + +[[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 `WebSecurityConfigurerAdapter` that configures the app as a resource server. +When use Opaque Token, this `WebSecurityConfigurerAdapter` looks like: + +.Default Opaque Token Configuration +==== +.Java +[source,java,role="primary"] +---- +protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + opaqueToken { } + } + } +} +---- +==== + +If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one. + +Replacing this is as simple as exposing the bean within the application: + +.Custom Opaque Token Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .opaqueToken(opaqueToken -> opaqueToken + .introspector(myIntrospector()) + ) + ); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/messages/**", hasAuthority("SCOPE_message:read")) + authorize(anyRequest, 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. + +[[oauth2resourceserver-opaque-introspector]] +For example, the second `@Bean` Spring Boot creates is an `OpaqueTokenIntrospector`, <>: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OpaqueTokenIntrospector introspector() { + return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(): OpaqueTokenIntrospector { + return NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret) +} +---- +==== + +If the application doesn't expose a <> bean, then Spring Boot will expose the above default one. + +And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`. + +Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a <> can be specified in XML. + +The filter chain is specified like so: + +.Default Opaque Token Configuration +==== +.Xml +[source,xml,role="primary"] +---- + + + + + + +---- +==== + +And the <> like so: + +.Opaque Token Introspector +==== +.Xml +[source,xml,role="primary"] +---- + + + + + +---- +==== + +[[oauth2resourceserver-opaque-introspectionuri-dsl]] +=== Using `introspectionUri()` + +An authorization server's Introspection Uri can be configured <> or it can be supplied in the DSL: + +.Introspection URI Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .opaqueToken(opaqueToken -> opaqueToken + .introspectionUri("https://idp.example.com/introspect") + .introspectionClientCredentials("client", "secret") + ) + ); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class DirectlyConfiguredIntrospectionUri : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspectionUri = "https://idp.example.com/introspect" + introspectionClientCredentials("client", "secret") + } + } + } + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + +---- +==== + +Using `introspectionUri()` takes precedence over any configuration property. + +[[oauth2resourceserver-opaque-introspector-dsl]] +=== Using `introspector()` + +More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of <>: + +.Introspector Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .opaqueToken(opaqueToken -> opaqueToken + .introspector(myCustomIntrospector()) + ) + ); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class DirectlyConfiguredIntrospector : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspector = myCustomIntrospector() + } + } + } + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + +---- +==== + +This is handy when deeper configuration, like <>, <>, or <>, is necessary. + +[[oauth2resourceserver-opaque-introspector-bean]] +=== Exposing a `OpaqueTokenIntrospector` `@Bean` + +Or, exposing a <> `@Bean` has the same effect as `introspector()`: + +[source,java] +---- +@Bean +public OpaqueTokenIntrospector introspector() { + return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); +} +---- + +[[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: + +.Authorization Opaque Token Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class MappedAuthorities extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests(authorizeRequests -> authorizeRequests + .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class MappedAuthorities : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize("/contacts/**", hasAuthority("SCOPE_contacts")) + authorize("/messages/**", hasAuthority("SCOPE_messages")) + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + opaqueToken { } + } + } + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + +---- +==== + +Or similarly with method security: + +==== +.Java +[source,java,role="primary"] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +public List getMessages(...) {} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +fun getMessages(): List {} +---- +==== + +[[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 <> that takes a look at the attribute set and converts in its own way: + +==== +.Java +[source,java,role="primary"] +---- +public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector { + private OpaqueTokenIntrospector delegate = + new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); + + public OAuth2AuthenticatedPrincipal introspect(String token) { + OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token); + return new DefaultOAuth2AuthenticatedPrincipal( + principal.getName(), principal.getAttributes(), extractAuthorities(principal)); + } + + private Collection extractAuthorities(OAuth2AuthenticatedPrincipal principal) { + List scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE); + return scopes.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class CustomAuthoritiesOpaqueTokenIntrospector : OpaqueTokenIntrospector { + private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + override fun introspect(token: String): OAuth2AuthenticatedPrincipal { + val principal: OAuth2AuthenticatedPrincipal = delegate.introspect(token) + return DefaultOAuth2AuthenticatedPrincipal( + principal.name, principal.attributes, extractAuthorities(principal)) + } + + private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection { + val scopes: List = principal.getAttribute(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 OpaqueTokenIntrospector introspector() { + return new CustomAuthoritiesOpaqueTokenIntrospector(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(): OpaqueTokenIntrospector { + return CustomAuthoritiesOpaqueTokenIntrospector() +} +---- +==== + +[[oauth2resourceserver-opaque-timeouts]] +== Configuring Timeouts + +By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server. + +This may be too short in some scenarios. +Further, it doesn't take into account more sophisticated patterns like back-off and discovery. + +To adjust the way in which Resource Server connects to the authorization server, `NimbusOpaqueTokenIntrospector` accepts an instance of `RestOperations`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder, OAuth2ResourceServerProperties properties) { + RestOperations rest = builder + .basicAuthentication(properties.getOpaquetoken().getClientId(), properties.getOpaquetoken().getClientSecret()) + .setConnectTimeout(Duration.ofSeconds(60)) + .setReadTimeout(Duration.ofSeconds(60)) + .build(); + + return new NimbusOpaqueTokenIntrospector(introspectionUri, rest); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(builder: RestTemplateBuilder, properties: OAuth2ResourceServerProperties): OpaqueTokenIntrospector? { + val rest: RestOperations = builder + .basicAuthentication(properties.opaquetoken.clientId, properties.opaquetoken.clientSecret) + .setConnectTimeout(Duration.ofSeconds(60)) + .setReadTimeout(Duration.ofSeconds(60)) + .build() + return NimbusOpaqueTokenIntrospector(introspectionUri, rest) +} +---- +==== + +[[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 <> 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 OpaqueTokenIntrospector { + private OpaqueTokenIntrospector delegate = + new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); + private JwtDecoder jwtDecoder = new NimbusJwtDecoder(new ParseOnlyJWTProcessor()); + + public OAuth2AuthenticatedPrincipal introspect(String token) { + OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token); + try { + Jwt jwt = this.jwtDecoder.decode(token); + return new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES); + } catch (JwtException ex) { + throw new OAuth2IntrospectionException(ex); + } + } + + private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor { + JWTClaimsSet process(SignedJWT jwt, SecurityContext context) + throws JOSEException { + return jwt.getJWTClaimsSet(); + } + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class JwtOpaqueTokenIntrospector : OpaqueTokenIntrospector { + private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val jwtDecoder: JwtDecoder = NimbusJwtDecoder(ParseOnlyJWTProcessor()) + override fun introspect(token: String): OAuth2AuthenticatedPrincipal { + val principal = delegate.introspect(token) + return try { + val jwt: Jwt = jwtDecoder.decode(token) + DefaultOAuth2AuthenticatedPrincipal(jwt.claims, NO_AUTHORITIES) + } catch (ex: JwtException) { + throw OAuth2IntrospectionException(ex.message) + } + } + + private class ParseOnlyJWTProcessor : DefaultJWTProcessor() { + override fun process(jwt: SignedJWT, context: SecurityContext): JWTClaimsSet { + return jwt.jwtClaimsSet + } + } +} +---- +==== + +Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OpaqueTokenIntrospector introspector() { + return new JwtOpaqueTokenIntrospector(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(): OpaqueTokenIntrospector { + return JwtOpaqueTokenIntrospector() +} +---- +==== + +[[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 <>. +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 OpaqueTokenIntrospector { + private final OpaqueTokenIntrospector delegate = + new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); + private final OAuth2UserService oauth2UserService = new DefaultOAuth2UserService(); + + private final ClientRegistrationRepository repository; + + // ... constructor + + @Override + public OAuth2AuthenticatedPrincipal introspect(String token) { + OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token); + Instant issuedAt = authorized.getAttribute(ISSUED_AT); + Instant expiresAt = authorized.getAttribute(EXPIRES_AT); + ClientRegistration clientRegistration = this.repository.findByRegistrationId("registration-id"); + OAuth2AccessToken token = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt); + OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(clientRegistration, token); + return this.oauth2UserService.loadUser(oauth2UserRequest); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class UserInfoOpaqueTokenIntrospector : OpaqueTokenIntrospector { + private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val oauth2UserService = DefaultOAuth2UserService() + private val repository: ClientRegistrationRepository? = null + + // ... constructor + + override fun introspect(token: String): OAuth2AuthenticatedPrincipal { + val authorized = delegate.introspect(token) + val issuedAt: Instant? = authorized.getAttribute(ISSUED_AT) + val expiresAt: Instant? = authorized.getAttribute(EXPIRES_AT) + val clientRegistration: ClientRegistration = repository!!.findByRegistrationId("registration-id") + val accessToken = OAuth2AccessToken(BEARER, token, issuedAt, expiresAt) + val oauth2UserRequest = OAuth2UserRequest(clientRegistration, accessToken) + return oauth2UserService.loadUser(oauth2UserRequest) + } +} +---- +==== + +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 OpaqueTokenIntrospector { + private final OpaqueTokenIntrospector delegate = + new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); + private final WebClient rest = WebClient.create(); + + @Override + public OAuth2AuthenticatedPrincipal introspect(String token) { + OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token); + return makeUserInfoRequest(authorized); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class UserInfoOpaqueTokenIntrospector : OpaqueTokenIntrospector { + private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val rest: WebClient = WebClient.create() + + override fun introspect(token: String): OAuth2AuthenticatedPrincipal { + val authorized = delegate.introspect(token) + return makeUserInfoRequest(authorized) + } +} +---- +==== + +Either way, having created your <>, you should publish it as a `@Bean` to override the defaults: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +OpaqueTokenIntrospector introspector() { + return new UserInfoOpaqueTokenIntrospector(...); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(): OpaqueTokenIntrospector { + return UserInfoOpaqueTokenIntrospector(...) +} +---- +====