diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/preface/java-configuration.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/preface/java-configuration.adoc index 1a72196ffd..4c2eacdf40 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/preface/java-configuration.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/preface/java-configuration.adoc @@ -400,1155 +400,7 @@ include::oauth2-client.adoc[] include::oauth2-login.adoc[] - -[[oauth2resourceserver]] -== OAuth 2.0 Resource Server - -Spring Security supports protecting endpoints using two forms of OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]: - -* https://tools.ietf.org/html/rfc7519[JWT] -* Opaque Tokens - -This is handy in circumstances where an application has delegated its authority management to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity). -This authorization server can be consulted by resource servers to authorize requests. - -[NOTE] -==== -Working samples for both {gh-samples-url}/boot/oauth2resourceserver[JWTs] and {gh-samples-url}/boot/oauth2resourceserver-opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security repository]. -==== - -=== Dependencies - -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 ----- - -Where `https://idp.example.com` 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 `https://idp.example.com/.well-known/openid-configuration` 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. - -And that's it! - -==== Startup Expectations - -When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens. - -It achieves this through a deterministic startup process: - -1. Hit the Provider Configuration endpoint, `https://idp.example.com/.well-known/openid-configuration`, processing the response for the `jwks_url` property -2. Configure the validation strategy to query `jwks_url` for valid public keys -3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`. - -A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up. - -[NOTE] -If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail. - -==== Runtime Expectations - -Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header: - -[source,html] ----- -GET / HTTP/1.1 -Authorization: Bearer some-token-value # Resource Server will process this ----- - -So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification. - -Given a well-formed JWT, Resource Server will: - -1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header -2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and -3. Map each scope to an authority with the prefix `SCOPE_`. - -[NOTE] -As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens. - -The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present. - -From here, consider jumping to: - -<> - -<> - -[[oauth2resourceserver-jwt-jwkseturi]] -=== Specifying the Authorization Server JWK Set Uri Directly - -If the authorization server doesn't support the Provider Configuration endpoint, or if Resource Server must be able to start up independently from the authorization server, then `issuer-uri` can be exchanged for `jwk-set-uri`: - -[source,yaml] ----- -spring: - security: - oauth2: - resourceserver: - jwt: - 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. -However, it will also no longer validate the `iss` claim in the JWT (since Resource Server no longer knows what the issuer value should be). - -[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: - -[source,java] ----- -protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::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: - -[source,java] ----- -@EnableWebSecurity -public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") - .anyRequest().authenticated() - .and() - .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. - -For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`: - -[source,java] ----- -@Bean -public JwtDecoder jwtDecoder() { - return JwtDecoders.fromOidcIssuerLocation(issuerUri); -} ----- - -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()`. - -[[oauth2resourceserver-jwt-jwkseturi-dsl]] -==== Using `jwkSetUri()` - -An authorization server's JWK Set Uri can be configured <> or it can be supplied in the DSL: - -[source,java] ----- -@EnableWebSecurity -public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2ResourceServer() - .jwt() - .jwkSetUri("https://idp.example.com/.well-known/jwks.json"); - } -} ----- - -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 `JwtDecoder`: - -[source,java] ----- -@EnableWebSecurity -public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2ResourceServer() - .jwt() - .decoder(myCustomDecoder()); - } -} ----- - -This is handy when deeper configuration, like <>, <>, or <>, is necessary. - -[[oauth2resourceserver-jwt-decoder-bean]] -==== Exposing a `JwtDecoder` `@Bean` - -Or, exposing a `JwtDecoder` `@Bean` has the same effect as `decoder()`: - -[source,java] ----- -@Bean -public 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`: - -[source,java] ----- -@Bean -JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri) - .jwsAlgorithm(RS512).build(); -} ----- - -Calling `jwsAlgorithm` more than once will configure `NimbusJwtDecoder` to trust more than one algorithm, like so: - -[source,java] ----- -@Bean -JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri) - .jwsAlgorithm(RS512).jwsAlgorithm(EC512).build(); -} ----- - -Or, you can call `jwsAlgorithms`: - -[source,java] ----- -@Bean -JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri) - .jwsAlgorithms(algorithms -> { - algorithms.add(RS512); - algorithms.add(EC512); - }).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 -@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); -} -``` - -[[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`: - -[source,java] ----- -@Bean -BeanFactoryPostProcessor conversionServiceCustomizer() { - return beanFactory -> - beanFactory.getBean(RsaKeyConversionServicePostProcessor.class) - .setResourceLoader(new CustomResourceLoader()); -} ----- - -Specify your key's location: - -```yaml -key.location: hfds://my-key.pub -``` - -And then autowire the value: - -```java -@Value("${key.location}") -RSAPublicKey key; -``` - -[[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 -@Bean -public 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: - -[source,java] ----- -@Bean -public JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withSecretKey(this.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: - -[source,java] ----- -@EnableWebSecurity -public class DirectlyConfiguredJwkSetUri 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::jwt); - } -} ----- - -Or similarly with method security: - -[source,java] ----- -@PreAuthorize("hasAuthority('SCOPE_messages')") -public List getMessages(...) {} ----- - -[[oauth2resourceserver-jwt-authorization-extraction]] -==== Extracting Authorities Manually - -However, there are a number of circumstances where this default is insufficient. -For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute. -Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities. - -To this end, the DSL exposes `jwtAuthenticationConverter()`: - -[source,java] ----- -@EnableWebSecurity -public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2ResourceServer() - .jwt() - .jwtAuthenticationConverter(grantedAuthoritiesExtractor()); - } -} - -Converter grantedAuthoritiesExtractor() { - JwtAuthenticationConverter jwtAuthenticationConverter = - new JwtAuthenticationConverter(); - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter - (new GrantedAuthoritiesExtractor()); - return jwtAuthenticationConveter; -} ----- - -which is responsible for converting a `Jwt` into an `Authentication`. -As part of its configuration, we can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities. - -That final converter might be something like `GrantedAuthoritiesExtractor` below: - -[source,java] ----- -static class GrantedAuthoritiesExtractor - implements Converter> { - - public Collection convert(Jwt jwt) { - Collection authorities = (Collection) - jwt.getClaims().get("mycustomclaim"); - - return authorities.stream() - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - } -} ----- - -For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter`: - -[source,java] ----- -static class CustomAuthenticationConverter implements Converter { - public AbstractAuthenticationToken convert(Jwt jwt) { - return new CustomAuthenticationToken(jwt); - } -} ----- - -[[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: - -[source,java] ----- -@Bean -JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) - JwtDecoders.fromOidcIssuerLocation(issuerUri); - - OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator<>( - new JwtTimestampValidator(Duration.ofSeconds(60)), - new IssuerValidator(issuerUri)); - - jwtDecoder.setJwtValidator(withClockSkew); - - return jwtDecoder; -} ----- - -[NOTE] -By default, Resource Server configures a clock skew of 30 seconds. - -[[oauth2resourceserver-jwt-validation-custom]] -==== Configuring a Custom Validator - -Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API: - -[source,java] ----- -public class AudienceValidator implements OAuth2TokenValidator { - OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null); - - public OAuth2TokenValidatorResult validate(Jwt jwt) { - if (jwt.getAudience().contains("messaging")) { - return OAuth2TokenValidatorResult.success(); - } else { - return OAuth2TokenValidatorResult.failure(error); - } - } -} ----- - -Then, to add into a resource server, it's a matter of specifying the `JwtDecoder` instance: - -[source,java] ----- -@Bean -JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) - JwtDecoders.fromOidcIssuerLocation(issuerUri); - - OAuth2TokenValidator audienceValidator = new AudienceValidator(); - OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); - OAuth2TokenValidator withAudience = new 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 -@Bean -JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); - - MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter - .withDefaults(Collections.singletonMap("sub", 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 -MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value")); -``` - -[[oauth2resourceserver-jwt-claimsetmapping-remove]] -==== Removing a Claim - -And removing a claim is also simple, using the same API: - -```java -MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> 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 -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; - } -} -``` - -And then, the instance can be supplied like normal: - -```java -@Bean -JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); - jwtDecoder.setClaimSetConverter(new 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 -@Bean -public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { - RestOperations rest = builder - .setConnectionTimeout(60000) - .setReadTimeout(60000) - .build(); - - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build(); - return jwtDecoder; -} -``` - -[[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. - -==== 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: - -```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 `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-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: - -[source,java] ----- -@GetMapping("/foo") -public String foo(BearerTokenAuthentication authentication) { - return authentication.getTokenAttributes().get("sub") + " is the subject"; -} ----- - -Since `BearerTokenAuthentication` holds an `OAuth2AuthenticatedPrincipal`, that also means that it's available to controller methods, too: - -[source,java] ----- -@GetMapping("/foo") -public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) { - return principal.getAttribute("sub") + " 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 -@PreAuthorize("principal?.attributes['sub'] == 'foo'") -public String forFoosEyesOnly() { - 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: - -[source,java] ----- -protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::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: - -[source,java] ----- -@EnableWebSecurity -public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") - .anyRequest().authenticated() - .and() - .oauth2ResourceServer() - .opaqueToken() - .introspector(myIntrospector()); - } -} ----- - -The above requires the scope of `message:read` for any URL that starts with `/messages/`. - -Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration. - -For example, the second `@Bean` Spring Boot creates is an `OpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`: - -[source,java] ----- -@Bean -public OpaqueTokenIntrospector introspector() { - return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); -} ----- - -If the application doesn't expose a `OpaqueTokenIntrospector` bean, then Spring Boot will expose the above default one. - -And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`. - -[[oauth2resourceserver-opaque-introspectionuri-dsl]] -==== Using `introspectionUri()` - -An authorization server's Introspection Uri can be configured <> or it can be supplied in the DSL: - -[source,java] ----- -@EnableWebSecurity -public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2ResourceServer() - .opaqueToken() - .introspectionUri("https://idp.example.com/introspect") - .introspectionClientCredentials("client", "secret"); - } -} ----- - -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 `OpaqueTokenIntrospector`: - -[source,java] ----- -@EnableWebSecurity -public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2ResourceServer() - .opaqueToken() - .introspector(myCustomIntrospector()); - } -} ----- - -This is handy when deeper configuration, like <>, <>, or <>, is necessary. - -[[oauth2resourceserver-opaque-introspector-bean]] -==== Exposing a `OpaqueTokenIntrospector` `@Bean` - -Or, exposing a `OpaqueTokenIntrospector` `@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: - -```java -@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); - } -} -``` - -Or similarly with method security: - -```java -@PreAuthorize("hasAuthority('SCOPE_messages')") -public List getMessages(...) {} -``` - -[[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 `OpaqueTokenIntrospector` that takes a look at the attribute set and converts in its own way: - -[source,java] ----- -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()); - } -} ----- - -Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`: - -[source,java] ----- -@Bean -public OpaqueTokenIntrospector introspector() { - return new 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 -@Bean -public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder) { - RestOperations rest = builder - .basicAuthentication(clientId, clientSecret) - .setConnectionTimeout(60000) - .setReadTimeout(60000) - .build(); - - return new 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 `OpaqueTokenIntrospector` that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes: - -[source,java] ----- -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 e) { - throw new OAuth2IntrospectionException(e); - } - } - - private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor { - JWTClaimsSet process(SignedJWT jwt, SecurityContext context) - throws JOSEException { - return jwt.getJWTClaimSet(); - } - } -} ----- - -Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`: - -[source,java] ----- -@Bean -public OpaqueTokenIntrospector introspector() { - return new JwtOpaqueTokenIntropsector(); -} ----- - -[[oauth2resourceserver-opaque-userinfo]] -=== Calling a `/userinfo` Endpoint - -Generally speaking, a Resource Server doesn't care about the underlying user, but instead about the authorities that have been granted. - -That said, at times it can be valuable to tie the authorization statement back to a user. - -If an application is also using `spring-security-oauth2-client`, having set up the appropriate `ClientRegistrationRepository`, then this is quite simple with a custom `OpaqueTokenIntrospector`. -This implementation below does three things: - -* Delegates to the introspection endpoint, to affirm the token's validity -* Looks up the appropriate client registration associated with the `/userinfo` endpoint -* Invokes and returns the response from the `/userinfo` endpoint - -[source,java] ----- -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); - } -} ----- - -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`: - -[source,java] ----- -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); - } -} ----- - -Either way, having created your `OpaqueTokenIntrospector`, you should publish it as a `@Bean` to override the defaults: - -[source,java] ----- -@Bean -OpaqueTokenIntrospector introspector() { - return new UserInfoOpaqueTokenIntrospector(...); -} ----- - -Thus far we have only taken a look at the most basic authentication configuration. -Let's take a look at a few slightly more advanced options for configuring authentication. +include::oauth2-resourceserver.adoc[] [[jc-authentication-inmemory]] === In-Memory Authentication diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/preface/oauth2-resourceserver.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/preface/oauth2-resourceserver.adoc new file mode 100644 index 0000000000..c45c763b6f --- /dev/null +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/preface/oauth2-resourceserver.adoc @@ -0,0 +1,1148 @@ +[[oauth2resourceserver]] +== OAuth 2.0 Resource Server + +Spring Security supports protecting endpoints using two forms of OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]: + +* https://tools.ietf.org/html/rfc7519[JWT] +* Opaque Tokens + +This is handy in circumstances where an application has delegated its authority management to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity). +This authorization server can be consulted by resource servers to authorize requests. + +[NOTE] +==== +Working samples for both {gh-samples-url}/boot/oauth2resourceserver[JWTs] and {gh-samples-url}/boot/oauth2resourceserver-opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security repository]. +==== + +=== Dependencies + +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 +---- + +Where `https://idp.example.com` 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 `https://idp.example.com/.well-known/openid-configuration` 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. + +And that's it! + +==== Startup Expectations + +When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens. + +It achieves this through a deterministic startup process: + +1. Hit the Provider Configuration endpoint, `https://idp.example.com/.well-known/openid-configuration`, processing the response for the `jwks_url` property +2. Configure the validation strategy to query `jwks_url` for valid public keys +3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`. + +A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up. + +[NOTE] +If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail. + +==== Runtime Expectations + +Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header: + +[source,html] +---- +GET / HTTP/1.1 +Authorization: Bearer some-token-value # Resource Server will process this +---- + +So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification. + +Given a well-formed JWT, Resource Server will: + +1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header +2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and +3. Map each scope to an authority with the prefix `SCOPE_`. + +[NOTE] +As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens. + +The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present. + +From here, consider jumping to: + +<> + +<> + +[[oauth2resourceserver-jwt-jwkseturi]] +=== Specifying the Authorization Server JWK Set Uri Directly + +If the authorization server doesn't support the Provider Configuration endpoint, or if Resource Server must be able to start up independently from the authorization server, then `issuer-uri` can be exchanged for `jwk-set-uri`: + +[source,yaml] +---- +spring: + security: + oauth2: + resourceserver: + jwt: + 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. +However, it will also no longer validate the `iss` claim in the JWT (since Resource Server no longer knows what the issuer value should be). + +[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: + +[source,java] +---- +protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::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: + +[source,java] +---- +@EnableWebSecurity +public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") + .anyRequest().authenticated() + .and() + .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. + +For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`: + +[source,java] +---- +@Bean +public JwtDecoder jwtDecoder() { + return JwtDecoders.fromOidcIssuerLocation(issuerUri); +} +---- + +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()`. + +[[oauth2resourceserver-jwt-jwkseturi-dsl]] +==== Using `jwkSetUri()` + +An authorization server's JWK Set Uri can be configured <> or it can be supplied in the DSL: + +[source,java] +---- +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .jwkSetUri("https://idp.example.com/.well-known/jwks.json"); + } +} +---- + +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 `JwtDecoder`: + +[source,java] +---- +@EnableWebSecurity +public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .decoder(myCustomDecoder()); + } +} +---- + +This is handy when deeper configuration, like <>, <>, or <>, is necessary. + +[[oauth2resourceserver-jwt-decoder-bean]] +==== Exposing a `JwtDecoder` `@Bean` + +Or, exposing a `JwtDecoder` `@Bean` has the same effect as `decoder()`: + +[source,java] +---- +@Bean +public 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`: + +[source,java] +---- +@Bean +JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).build(); +} +---- + +Calling `jwsAlgorithm` more than once will configure `NimbusJwtDecoder` to trust more than one algorithm, like so: + +[source,java] +---- +@Bean +JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).jwsAlgorithm(EC512).build(); +} +---- + +Or, you can call `jwsAlgorithms`: + +[source,java] +---- +@Bean +JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri) + .jwsAlgorithms(algorithms -> { + algorithms.add(RS512); + algorithms.add(EC512); + }).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 +@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); +} +``` + +[[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`: + +[source,java] +---- +@Bean +BeanFactoryPostProcessor conversionServiceCustomizer() { + return beanFactory -> + beanFactory.getBean(RsaKeyConversionServicePostProcessor.class) + .setResourceLoader(new CustomResourceLoader()); +} +---- + +Specify your key's location: + +```yaml +key.location: hfds://my-key.pub +``` + +And then autowire the value: + +```java +@Value("${key.location}") +RSAPublicKey key; +``` + +[[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 +@Bean +public 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: + +[source,java] +---- +@Bean +public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withSecretKey(this.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: + +[source,java] +---- +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri 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::jwt); + } +} +---- + +Or similarly with method security: + +[source,java] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +public List getMessages(...) {} +---- + +[[oauth2resourceserver-jwt-authorization-extraction]] +==== Extracting Authorities Manually + +However, there are a number of circumstances where this default is insufficient. +For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute. +Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities. + +To this end, the DSL exposes `jwtAuthenticationConverter()`: + +[source,java] +---- +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .jwtAuthenticationConverter(grantedAuthoritiesExtractor()); + } +} + +Converter grantedAuthoritiesExtractor() { + JwtAuthenticationConverter jwtAuthenticationConverter = + new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter + (new GrantedAuthoritiesExtractor()); + return jwtAuthenticationConveter; +} +---- + +which is responsible for converting a `Jwt` into an `Authentication`. +As part of its configuration, we can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities. + +That final converter might be something like `GrantedAuthoritiesExtractor` below: + +[source,java] +---- +static class GrantedAuthoritiesExtractor + implements Converter> { + + public Collection convert(Jwt jwt) { + Collection authorities = (Collection) + jwt.getClaims().get("mycustomclaim"); + + return authorities.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +} +---- + +For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter`: + +[source,java] +---- +static class CustomAuthenticationConverter implements Converter { + public AbstractAuthenticationToken convert(Jwt jwt) { + return new CustomAuthenticationToken(jwt); + } +} +---- + +[[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: + +[source,java] +---- +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) + JwtDecoders.fromOidcIssuerLocation(issuerUri); + + OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator<>( + new JwtTimestampValidator(Duration.ofSeconds(60)), + new IssuerValidator(issuerUri)); + + jwtDecoder.setJwtValidator(withClockSkew); + + return jwtDecoder; +} +---- + +[NOTE] +By default, Resource Server configures a clock skew of 30 seconds. + +[[oauth2resourceserver-jwt-validation-custom]] +==== Configuring a Custom Validator + +Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API: + +[source,java] +---- +public class AudienceValidator implements OAuth2TokenValidator { + OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null); + + public OAuth2TokenValidatorResult validate(Jwt jwt) { + if (jwt.getAudience().contains("messaging")) { + return OAuth2TokenValidatorResult.success(); + } else { + return OAuth2TokenValidatorResult.failure(error); + } + } +} +---- + +Then, to add into a resource server, it's a matter of specifying the `JwtDecoder` instance: + +[source,java] +---- +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) + JwtDecoders.fromOidcIssuerLocation(issuerUri); + + OAuth2TokenValidator audienceValidator = new AudienceValidator(); + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); + OAuth2TokenValidator withAudience = new 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 +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); + + MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter + .withDefaults(Collections.singletonMap("sub", 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 +MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value")); +``` + +[[oauth2resourceserver-jwt-claimsetmapping-remove]] +==== Removing a Claim + +And removing a claim is also simple, using the same API: + +```java +MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> 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 +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; + } +} +``` + +And then, the instance can be supplied like normal: + +```java +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); + jwtDecoder.setClaimSetConverter(new 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 +@Bean +public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { + RestOperations rest = builder + .setConnectionTimeout(60000) + .setReadTimeout(60000) + .build(); + + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build(); + return jwtDecoder; +} +``` + +[[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. + +==== 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: + +```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 `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-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: + +[source,java] +---- +@GetMapping("/foo") +public String foo(BearerTokenAuthentication authentication) { + return authentication.getTokenAttributes().get("sub") + " is the subject"; +} +---- + +Since `BearerTokenAuthentication` holds an `OAuth2AuthenticatedPrincipal`, that also means that it's available to controller methods, too: + +[source,java] +---- +@GetMapping("/foo") +public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) { + return principal.getAttribute("sub") + " 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 +@PreAuthorize("principal?.attributes['sub'] == 'foo'") +public String forFoosEyesOnly() { + 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: + +[source,java] +---- +protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::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: + +[source,java] +---- +@EnableWebSecurity +public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .opaqueToken() + .introspector(myIntrospector()); + } +} +---- + +The above requires the scope of `message:read` for any URL that starts with `/messages/`. + +Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration. + +For example, the second `@Bean` Spring Boot creates is an `OpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`: + +[source,java] +---- +@Bean +public OpaqueTokenIntrospector introspector() { + return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); +} +---- + +If the application doesn't expose a `OpaqueTokenIntrospector` bean, then Spring Boot will expose the above default one. + +And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`. + +[[oauth2resourceserver-opaque-introspectionuri-dsl]] +==== Using `introspectionUri()` + +An authorization server's Introspection Uri can be configured <> or it can be supplied in the DSL: + +[source,java] +---- +@EnableWebSecurity +public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .opaqueToken() + .introspectionUri("https://idp.example.com/introspect") + .introspectionClientCredentials("client", "secret"); + } +} +---- + +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 `OpaqueTokenIntrospector`: + +[source,java] +---- +@EnableWebSecurity +public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .opaqueToken() + .introspector(myCustomIntrospector()); + } +} +---- + +This is handy when deeper configuration, like <>, <>, or <>, is necessary. + +[[oauth2resourceserver-opaque-introspector-bean]] +==== Exposing a `OpaqueTokenIntrospector` `@Bean` + +Or, exposing a `OpaqueTokenIntrospector` `@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: + +```java +@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); + } +} +``` + +Or similarly with method security: + +```java +@PreAuthorize("hasAuthority('SCOPE_messages')") +public List getMessages(...) {} +``` + +[[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 `OpaqueTokenIntrospector` that takes a look at the attribute set and converts in its own way: + +[source,java] +---- +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()); + } +} +---- + +Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`: + +[source,java] +---- +@Bean +public OpaqueTokenIntrospector introspector() { + return new 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 +@Bean +public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder) { + RestOperations rest = builder + .basicAuthentication(clientId, clientSecret) + .setConnectionTimeout(60000) + .setReadTimeout(60000) + .build(); + + return new 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 `OpaqueTokenIntrospector` that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes: + +[source,java] +---- +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 e) { + throw new OAuth2IntrospectionException(e); + } + } + + private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor { + JWTClaimsSet process(SignedJWT jwt, SecurityContext context) + throws JOSEException { + return jwt.getJWTClaimSet(); + } + } +} +---- + +Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`: + +[source,java] +---- +@Bean +public OpaqueTokenIntrospector introspector() { + return new JwtOpaqueTokenIntropsector(); +} +---- + +[[oauth2resourceserver-opaque-userinfo]] +=== Calling a `/userinfo` Endpoint + +Generally speaking, a Resource Server doesn't care about the underlying user, but instead about the authorities that have been granted. + +That said, at times it can be valuable to tie the authorization statement back to a user. + +If an application is also using `spring-security-oauth2-client`, having set up the appropriate `ClientRegistrationRepository`, then this is quite simple with a custom `OpaqueTokenIntrospector`. +This implementation below does three things: + +* Delegates to the introspection endpoint, to affirm the token's validity +* Looks up the appropriate client registration associated with the `/userinfo` endpoint +* Invokes and returns the response from the `/userinfo` endpoint + +[source,java] +---- +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); + } +} +---- + +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`: + +[source,java] +---- +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); + } +} +---- + +Either way, having created your `OpaqueTokenIntrospector`, you should publish it as a `@Bean` to override the defaults: + +[source,java] +---- +@Bean +OpaqueTokenIntrospector introspector() { + return new UserInfoOpaqueTokenIntrospector(...); +} +---- + +Thus far we have only taken a look at the most basic authentication configuration. +Let's take a look at a few slightly more advanced options for configuring authentication.