From 2307b01a7a710caf1fe5619bd9db5ac28caebabe Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 3 Oct 2018 14:08:14 -0600 Subject: [PATCH] Resource Server Docs - Servlet Fixes: gh-5912 --- .../servlet/preface/java-configuration.adoc | 489 ++++++++++++++++++ 1 file changed, 489 insertions(+) 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 3dffb792f0..6187c5cd88 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 @@ -920,6 +920,495 @@ The following additional resources describe advanced configuration options: ** <> ** <> +[[oauth2resourceserver]] +== OAuth 2.0 Resource Server + +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. + +=== 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-minimalconfiguration]] +=== 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. + +==== Specifying the Authorization Server + +To specify which authorization server to use, simply do: + +```yaml +security: + oauth2: + resourceserver: + jwt: + issuer-uri: https://the.issuer.location +``` + +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. + +[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. +This endpoint is referred to as a https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Provider Configuration] endpoint. + +==== 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://the.issuer.location`. + +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 `Authorizatization: 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 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. + +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. + +From here, consider jumping to: + +<> + +<> + +[[oauth2resourceserver-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`: + +```yaml +security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: https://the.issuer.location/.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-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: + +```java +protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt(); +} +``` + +If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one. + +Replacing this is as simple as exposing the bean within the application: + +```java +@EnableWebSecurity +public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .mvcMatchers("/admin/**").hasAuthority("SCOPE_admin") + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .jwtAuthenticationConverter(myConverter()); + } +} +``` + +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`: + +```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-jwkseturi-dsl]] +==== Using `jwkSetUri()` + +An authorization server's JWK Set Uri can be configured <> or it can be supplied in the DSL: + +```java +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .jwkSetUri("https://the.issuer.location/.well-known/jwks.json"); + } +} +``` + +Using `jwkSetUri()` takes precedence over any configuration property. + +[[oauth2resourceserver-decoder-dsl]] +==== Using `decoder()` + +More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`: + +```java +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri 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-decoder-bean]] +==== Exposing a `JwtDecoder` `@Bean` + +Or, exposing a `JwtDecoder` `@Bean` has the same effect as `decoder()`: + +```java +@Bean +public JwtDecoder jwtDecoder() { + return new NimbusJwtDecoderJwkSupport(jwkSetUri); +} +``` + +[[oauth2resourceserver-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 prefix "SCOPE_". + +This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix: + +```java +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt(); + } +} +``` + +Or similarly with method security: + +```java +@PreAuthorize("hasAuthority('SCOPE_messages')") +public List getMessages(...) {} +``` + +[[oauth2resourceserver-authorization-extraction]] +==== 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. +Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities. + +To this end, the DSL exposes `jwtAuthenticationConverter()`: + +```java +@EnableWebSecurity +public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt() + .jwtAuthenticationConverter(grantedAuthoritiesExtractor()); + } +} + +Converter grantedAuthoritiesExtractor() { + return new GrantedAuthoritiesExtractor(); +} +``` + +which is responsible for converting a `Jwt` into an `Authentication`. + +We can override this quite simply to alter the way granted authorities are derived: + +```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`: + +```java +static class CustomAuthenticationConverter implements Converter { + public AbstractAuthenticationToken convert(Jwt jwt) { + return new CustomAuthenticationToken(jwt); + } +} +``` + +[[oauth2resourceserver-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-validation-clockskew]] +==== Customizing Timestamp Validation + +JWT's typically have a window of validity, with the start of the window indicated in the `nbf` claim and the end indicated in the `exp` claim. + +However, every server can experience clock drift, which can cause tokens to appear expired to one server, but not to another. +This can cause some implementation heartburn as the number of collaborating servers increases in a distributed system. + +Resource Server uses `JwtTimestampValidator` to verify a token's validity window, and it can be configured with a `clockSkew` to alleviate the above problem: + +```java +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport) + JwtDecoders.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. + +[[oauth2resourceserver-validation-custom]] +==== Configuring a Custom Validator + +Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API: + +```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: + +```java +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport) + JwtDecoders.withOidcIssuerLocation(issuerUri); + + OAuth2TokenValidator audienceValidator = new AudienceValidator(); + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); + OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); + + jwtDecoder.setJwtValidator(withAudience); + + return jwtDecoder; +} +``` + +[[oauth2resourceserver-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-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` | `URL` +| `jti` | `String` +| `nbf` | `Instant` +| `sub` | `String` +|============ + +An individual claim's conversion strategy can be configured using `MappedJwtClaimSetConverter.withDefaults`: + +```java +@Bean +JwtDecoder jwtDecoder() { + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri); + + MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter + .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub)); + jwtDecoder.setJwtClaimSetConverter(converter); + + return jwtDecoder; +} +``` +This will keep all the defaults, except it will override the default claim converter for `sub`. + +[[oauth2resourceserver-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-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-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() { + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri); + jwtDecoder.setJwtClaimSetConverter(new UsernameSubClaimAdapter()); + return jwtDecoder; +} +``` + +[[oauth2resourceserver-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, `NimbusJwtDecoderJwkSupport` accepts an instance of `RestOperations`: + +```java +@Bean +public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { + RestOperations rest = builder + .setConnectionTimeout(60000) + .setReadTimeout(60000) + .build(); + + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri); + jwtDecoder.setRestOperations(rest); + return jwtDecoder; +} +``` + [[jc-authentication]] == Authentication