From 78e27ca17f14d54acee51e5f1ad7d40995ecd260 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 11 Oct 2018 13:40:36 -0600 Subject: [PATCH] Update Reactive Resource Server Docs Resource Server documentation for both Servlet and Reactive now have a similar feel and offer deeper exposure to common use cases. Fixes: gh-6054 --- .../reactive/oauth2/resource-server.adoc | 367 +++++++++++++++++- .../servlet/preface/java-configuration.adoc | 48 ++- 2 files changed, 389 insertions(+), 26 deletions(-) diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc index ece9727310..b43f1f1ab2 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc @@ -1,16 +1,30 @@ [[webflux-oauth2-resource-server]] = OAuth2 Resource Server -Spring Security provides OAuth2 Resource Server support with JWT tokens. +Spring Security supports protecting endpoints using https://tools.ietf.org/html/rfc7519[JWT]-encoded OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]. +This is handy in circumstances where an application has federated its authority management out 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 validate authority when serving requests. [NOTE] ==== A complete working example can be found in {gh-samples-url}/boot/oauth2resourceserver-webflux[*OAuth 2.0 Resource Server WebFlux sample*]. ==== -The first step is to expose a `ReactiveJwtDecoder` as a `@Bean`. -In a Spring Boot application this can be done using: +== 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. + +[[webflux-oauth2-resource-server-minimal-configuration]] +== Minimal Configuration + +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. + +=== Specify the Authorization Server + +In a Spring Boot application, to specify which authorization server to use, simply do: [source,yml] ---- @@ -19,15 +33,107 @@ spring: oauth2: resourceserver: jwt: - issuer-uri: https://idp.example.com/auth/realms/demo + issuer-uri: https://idp.example.com ---- -The `issuer-uri` instructs Spring Security to leverage the endpoint at `https://idp.example.com/auth/realms/demo/.well-known/openid-configuration` to discover the configuration. -The above is all that is necessary to get a minimal Resource Server configured. -When new keys are made available, Spring Security will automatically rotate the keys used to validate the JWT tokens. +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. -By default each scope is mapped to an authority with the prefix `SCOPE_`. -For example, the following requires the scope of `message:read` for any URL that starts with `/messages/`. +[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://the.issuer.location/.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 token, 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. + +<> + +<> + +[[webflux-oauth2-resource-server-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] +---- +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 <>. + +[[webflux-oauth2-resource-server-sans-boot]] +=== Overriding or Replacing Boot Auto Configuration + +There are two `@Bean` s that Spring Boot generates on Resource Server's behalf. + +The first is a `SecurityWebFilterChain` that configures the app as a resource server: + +[source,java] +---- +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange() + .anyExchange().authenticated() + .and() + .oauth2ResourceServer() + .jwt(); + return http.build(); +} +---- + +If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one. + +Replacing this is as simple as exposing the bean within the application: [source,java] ---- @@ -44,4 +150,247 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { } ---- +The above requires the scope of `message:read` for any URL that starts with `/messages/`. + +Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration. + +For example, the second `@Bean` Spring Boot creates is a `ReactiveJwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`: + +[source,java] +---- +@Bean +public ReactiveJwtDecoder jwtDecoder() { + return ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri); +} +---- + +If the application doesn't expose a `ReactiveJwtDecoder` bean, then Spring Boot will expose the above default one. + +And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`. + +[[webflux-oauth2-resource-server-jwkseturi-dsl]] +==== Using `jwkSetUri()` + +An authorization server's JWK Set Uri can be configured <> or it can be supplied in the DSL: + +[source,java] +---- +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange() + .anyExchange().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .jwkSetUri("https://idp.example.com/.well-known/jwks.json"); + return http.build(); +} +---- + +Using `jwkSetUri()` takes precedence over any configuration property. + +[[webflux-oauth2-resource-server-decoder-dsl]] +==== Using `decoder()` + +More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`: + +[source,java] +---- +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange() + .anyExchange().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .decoder(myCustomDecoder()); + return http.build(); +} +---- + +This is handy when deeper configuration, like <>, is necessary. + +[[webflux-oauth2-resource-server-decoder-bean]] +==== Exposing a `ReactiveJwtDecoder` `@Bean` + +Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`: + +[source,java] +---- +@Bean +public JwtDecoder jwtDecoder() { + return new NimbusReactiveJwtDecoder(jwkSetUri); +} +---- + +[[webflux-oauth2-resource-server-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] +---- +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange() + .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") + .anyExchange().authenticated() + .and() + .oauth2ResourceServer() + .jwt(); + return http.build(); +} +---- + +Or similarly with method security: + +[source,java] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +public List getMessages(...) {} +---- + +[[webflux-oauth2-resource-server-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] +---- +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange() + .anyExchange().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .jwtAuthenticationConverter(grantedAuthoritiesExtractor()); + return http.build(); +} + +Converter> grantedAuthoritiesExtractor() { + GrantedAuthoritiesExtractor extractor = new GrantedAuthoritiesExtractor(); + return new ReactiveJwtAuthenticationConverterAdapter(extractor); +} +---- + +which is responsible for converting a `Jwt` into an `Authentication`. + +We can override this quite simply to alter the way granted authorities are derived: + +[source,java] +---- +static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter { + protected Collection extractAuthorities(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 Mono.just(jwt).map(this::doConversion); + } +} +---- + +[[webflux-oauth2-resource-server-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. + +[[webflux-oauth2-resource-server-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 +ReactiveJwtDecoder jwtDecoder() { + NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) + ReactiveJwtDecoders.withOidcIssuerLocation(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. + +[[webflux-oauth2-resource-server-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 `ReactiveJwtDecoder` instance: + +[source,java] +---- +@Bean +ReactiveJwtDecoder jwtDecoder() { + NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) + ReactiveJwtDecoders.withOidcIssuerLocation(issuerUri); + + OAuth2TokenValidator audienceValidator = new AudienceValidator(); + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); + OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); + + jwtDecoder.setJwtValidator(withAudience); + + return jwtDecoder; +} +---- 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 12988179ad..fb3a3b4291 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 @@ -397,6 +397,11 @@ Spring Security supports protecting endpoints using https://tools.ietf.org/html/ This is handy in circumstances where an application has federated its authority management out 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 validate authority when serving requests. +[NOTE] +==== +A complete working example can be found in {gh-samples-url}/boot/oauth2resourceserver[*OAuth 2.0 Resource Server Servlet sample*]. +==== + === Dependencies Most Resource Server support is collected into `spring-security-oauth2-resource-server`. @@ -417,25 +422,27 @@ security: oauth2: resourceserver: jwt: - issuer-uri: https://the.issuer.location + issuer-uri: https://idp.example.com ``` -Where `https://the.issuer.location` 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 and subsequently validate incoming JWTs. +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://the.issuer.location/.well-known/openid-configuration` is a supported endpoint for the authorization server. +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://the.issuer.location/.well-known/openid-configuration`, processing the response for the `jwks_url` property +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://the.issuer.location`. +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. @@ -444,7 +451,7 @@ If the authorization server is down when Resource Server queries it (given appro ==== Runtime Expectations -Once the application is started up, Resource Server will attempt to process any request containing an `Authorizatization: Bearer` header: +Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header: ```http GET / HTTP/1.1 @@ -453,10 +460,16 @@ 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 token, Resource Server will validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim. -It will also validate the signature against a public key obtained from the `jwks_url` endpoint and matched against the JWTs header. +Given a well-formed JWT token, Resource Server will -The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` map's to the JWT's `sub` property, if one is present. +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: @@ -474,7 +487,7 @@ security: oauth2: resourceserver: jwt: - jwk-set-uri: https://the.issuer.location/.well-known/jwks.json + jwk-set-uri: https://idp.example.com/.well-known/jwks.json ``` [NOTE] @@ -514,7 +527,7 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter protected void configure(HttpSecurity http) { http .authorizeRequests() - .mvcMatchers("/admin/**").hasAuthority("SCOPE_admin") + .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() .and() .oauth2ResourceServer() @@ -524,6 +537,8 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter } ``` +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`: @@ -554,7 +569,7 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { .and() .oauth2ResourceServer() .jwt() - .jwkSetUri("https://the.issuer.location/.well-known/jwks.json"); + .jwkSetUri("https://idp.example.com/.well-known/jwks.json"); } } ``` @@ -581,7 +596,7 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { } ``` -This is handy when deeper configuration, like <>, <>, or <>, is necessary. +This is handy when deeper configuration, like <>, <>, or <>, is necessary. [[oauth2resourceserver-decoder-bean]] ==== Exposing a `JwtDecoder` `@Bean` @@ -602,7 +617,7 @@ A JWT that is issued from an OAuth 2.0 Authorization Server will typically eithe `{ ..., "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 prefix "SCOPE_". +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: @@ -633,7 +648,7 @@ public List getMessages(...) {} ==== Extracting Authorities Manually However, there are a number of circumstances where this default is insufficient. -For example, some authorization server's don't use the `scope` attribute, but instead have their own custom attribute. +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()`: @@ -776,7 +791,6 @@ For these purposes, Resource Server supports mapping the JWT claim set with `Map By default, `MappedJwtClaimSetConverter` will attempt to coerce claims into the following types: - |============ | Claim | Java Type | `aud` | `Collection`