|
@@ -404,14 +404,17 @@ include::oauth2-login.adoc[]
|
|
|
[[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].
|
|
|
+Spring Security supports protecting endpoints using two forms of 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.
|
|
|
+* 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]
|
|
|
====
|
|
|
-A complete working example can be found in {gh-samples-url}/boot/oauth2resourceserver[*OAuth 2.0 Resource Server Servlet sample*].
|
|
|
+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
|
|
@@ -419,8 +422,8 @@ A complete working example can be found in {gh-samples-url}/boot/oauth2resources
|
|
|
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
|
|
|
+[[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.
|
|
@@ -472,7 +475,7 @@ 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
|
|
|
+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
|
|
@@ -485,11 +488,11 @@ The resulting `Authentication#getPrincipal`, by default, is a Spring Security `J
|
|
|
|
|
|
From here, consider jumping to:
|
|
|
|
|
|
-<<oauth2resourceserver-jwkseturi,How to Configure without Tying Resource Server startup to an authorization server's availability>>
|
|
|
+<<oauth2resourceserver-jwt-jwkseturi,How to Configure without Tying Resource Server startup to an authorization server's availability>>
|
|
|
|
|
|
-<<oauth2resourceserver-sansboot,How to Configure without Spring Boot>>
|
|
|
+<<oauth2resourceserver-jwt-sansboot,How to Configure without Spring Boot>>
|
|
|
|
|
|
-[[oauth2resourceserver-jwkseturi]]
|
|
|
+[[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`:
|
|
@@ -509,26 +512,22 @@ 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-jwkseturi-dsl,DSL>>.
|
|
|
+This property can also be supplied directly on the <<oauth2resourceserver-jwt-jwkseturi-dsl,DSL>>.
|
|
|
|
|
|
-[[oauth2resourceserver-sansboot]]
|
|
|
+[[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:
|
|
|
+The first is a `WebSecurityConfigurerAdapter` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `WebSecurityConfigurerAdapter` looks like:
|
|
|
|
|
|
```java
|
|
|
protected void configure(HttpSecurity http) {
|
|
|
http
|
|
|
- .authorizeRequests(authorizeRequests ->
|
|
|
- authorizeRequests
|
|
|
- .anyRequest().authenticated()
|
|
|
- )
|
|
|
- .oauth2ResourceServer(oauth2ResourceServer ->
|
|
|
- oauth2ResourceServer
|
|
|
- .jwt(withDefaults())
|
|
|
- );
|
|
|
+ .authorizeRequests()
|
|
|
+ .anyRequest().authenticated()
|
|
|
+ .and()
|
|
|
+ .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
|
|
}
|
|
|
```
|
|
|
|
|
@@ -541,18 +540,13 @@ Replacing this is as simple as exposing the bean within the application:
|
|
|
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
|
|
protected void configure(HttpSecurity http) {
|
|
|
http
|
|
|
- .authorizeRequests(authorizeRequests ->
|
|
|
- authorizeRequests
|
|
|
- .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
|
|
|
- .anyRequest().authenticated()
|
|
|
- )
|
|
|
- .oauth2ResourceServer(oauth2ResourceServer ->
|
|
|
- oauth2ResourceServer
|
|
|
- .jwt(jwt ->
|
|
|
- jwt
|
|
|
- .jwtAuthenticationConverter(myConverter())
|
|
|
- )
|
|
|
- );
|
|
|
+ .authorizeRequests()
|
|
|
+ .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
|
|
|
+ .anyRequest().authenticated()
|
|
|
+ .and()
|
|
|
+ .oauth2ResourceServer()
|
|
|
+ .jwt()
|
|
|
+ .jwtAuthenticationConverter(myConverter());
|
|
|
}
|
|
|
}
|
|
|
```
|
|
@@ -574,34 +568,29 @@ If the application doesn't expose a `JwtDecoder` bean, then Spring Boot will exp
|
|
|
|
|
|
And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`.
|
|
|
|
|
|
-[[oauth2resourceserver-jwkseturi-dsl]]
|
|
|
+[[oauth2resourceserver-jwt-jwkseturi-dsl]]
|
|
|
==== Using `jwkSetUri()`
|
|
|
|
|
|
-An authorization server's JWK Set Uri can be configured <<oauth2resourceserver-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
|
|
|
+An authorization server's JWK Set Uri can be configured <<oauth2resourceserver-jwt-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
|
|
|
|
|
|
```java
|
|
|
@EnableWebSecurity
|
|
|
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
|
|
protected void configure(HttpSecurity http) {
|
|
|
http
|
|
|
- .authorizeRequests(authorizeRequests ->
|
|
|
- authorizeRequests
|
|
|
- .anyRequest().authenticated()
|
|
|
- )
|
|
|
- .oauth2ResourceServer(oauth2ResourceServer ->
|
|
|
- oauth2ResourceServer
|
|
|
- .jwt(jwt ->
|
|
|
- jwt
|
|
|
- .jwkSetUri("https://idp.example.com/.well-known/jwks.json")
|
|
|
- )
|
|
|
- );
|
|
|
+ .authorizeRequests()
|
|
|
+ .anyRequest().authenticated()
|
|
|
+ .and()
|
|
|
+ .oauth2ResourceServer()
|
|
|
+ .jwt()
|
|
|
+ .jwkSetUri("https://idp.example.com/.well-known/jwks.json");
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Using `jwkSetUri()` takes precedence over any configuration property.
|
|
|
|
|
|
-[[oauth2resourceserver-decoder-dsl]]
|
|
|
+[[oauth2resourceserver-jwt-decoder-dsl]]
|
|
|
==== Using `decoder()`
|
|
|
|
|
|
More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`:
|
|
@@ -611,24 +600,19 @@ More powerful than `jwkSetUri()` is `decoder()`, which will completely replace a
|
|
|
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
|
|
protected void configure(HttpSecurity http) {
|
|
|
http
|
|
|
- .authorizeRequests(authorizeRequests ->
|
|
|
- authorizeRequests
|
|
|
- .anyRequest().authenticated()
|
|
|
- )
|
|
|
- .oauth2ResourceServer(oauth2ResourceServer ->
|
|
|
- oauth2ResourceServer
|
|
|
- .jwt(jwt ->
|
|
|
- jwt
|
|
|
- .decoder(myCustomDecoder())
|
|
|
- )
|
|
|
- );
|
|
|
+ .authorizeRequests()
|
|
|
+ .anyRequest().authenticated()
|
|
|
+ .and()
|
|
|
+ .oauth2ResourceServer()
|
|
|
+ .jwt()
|
|
|
+ .decoder(myCustomDecoder());
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-This is handy when deeper configuration, like <<oauth2resourceserver-validation,validation>>, <<oauth2resourceserver-claimsetmapping,mapping>>, or <<oauth2resourceserver-timeouts,request timeouts>>, is necessary.
|
|
|
+This is handy when deeper configuration, like <<oauth2resourceserver-jwt-validation,validation>>, <<oauth2resourceserver-jwt-claimsetmapping,mapping>>, or <<oauth2resourceserver-jwt-timeouts,request timeouts>>, is necessary.
|
|
|
|
|
|
-[[oauth2resourceserver-decoder-bean]]
|
|
|
+[[oauth2resourceserver-jwt-decoder-bean]]
|
|
|
==== Exposing a `JwtDecoder` `@Bean`
|
|
|
|
|
|
Or, exposing a `JwtDecoder` `@Bean` has the same effect as `decoder()`:
|
|
@@ -636,11 +620,11 @@ Or, exposing a `JwtDecoder` `@Bean` has the same effect as `decoder()`:
|
|
|
```java
|
|
|
@Bean
|
|
|
public JwtDecoder jwtDecoder() {
|
|
|
- return new NimbusJwtDecoder(JwtProcessors.withJwkSetUri(jwkSetUri).build());
|
|
|
+ return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-[[oauth2resourceserver-authorization]]
|
|
|
+[[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:
|
|
@@ -656,16 +640,12 @@ This means that to protect an endpoint or method with a scope derived from a JWT
|
|
|
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()
|
|
|
+ .authorizeRequests(authorizeRequests -> authorizeRequests
|
|
|
+ .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
|
|
|
+ .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
|
|
|
+ .anyRequest().authenticated()
|
|
|
)
|
|
|
- .oauth2ResourceServer(oauth2ResourceServer ->
|
|
|
- oauth2ResourceServer
|
|
|
- .jwt(withDefaults())
|
|
|
- );
|
|
|
+ .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
|
|
|
}
|
|
|
}
|
|
|
```
|
|
@@ -677,7 +657,7 @@ Or similarly with method security:
|
|
|
public List<Message> getMessages(...) {}
|
|
|
```
|
|
|
|
|
|
-[[oauth2resourceserver-authorization-extraction]]
|
|
|
+[[oauth2resourceserver-jwt-authorization-extraction]]
|
|
|
==== Extracting Authorities Manually
|
|
|
|
|
|
However, there are a number of circumstances where this default is insufficient.
|
|
@@ -691,17 +671,12 @@ To this end, the DSL exposes `jwtAuthenticationConverter()`:
|
|
|
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
|
|
protected void configure(HttpSecurity http) {
|
|
|
http
|
|
|
- .authorizeRequests(authorizeRequests ->
|
|
|
- authorizeRequests
|
|
|
- .anyRequest().authenticated()
|
|
|
- )
|
|
|
- .oauth2ResourceServer(oauth2ResourceServer ->
|
|
|
- oauth2ResourceServer
|
|
|
- .jwt(jwt ->
|
|
|
- jwt
|
|
|
- .jwtAuthenticationConverter(grantedAuthoritiesExtractor())
|
|
|
- )
|
|
|
- );
|
|
|
+ .authorizeRequests()
|
|
|
+ .anyRequest().authenticated()
|
|
|
+ .and()
|
|
|
+ .oauth2ResourceServer()
|
|
|
+ .jwt()
|
|
|
+ .jwtAuthenticationConverter(grantedAuthoritiesExtractor());
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -744,14 +719,14 @@ static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAut
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-[[oauth2resourceserver-validation]]
|
|
|
+[[oauth2resourceserver-jwt-validation]]
|
|
|
=== Configuring Validation
|
|
|
|
|
|
-Using <<oauth2resourceserver-minimalconfiguration,minimal Spring Boot configuration>>, 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.
|
|
|
+Using <<oauth2resourceserver-jwt-minimalconfiguration,minimal Spring Boot configuration>>, 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]]
|
|
|
+[[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.
|
|
@@ -780,7 +755,7 @@ JwtDecoder jwtDecoder() {
|
|
|
[NOTE]
|
|
|
By default, Resource Server configures a clock skew of 30 seconds.
|
|
|
|
|
|
-[[oauth2resourceserver-validation-custom]]
|
|
|
+[[oauth2resourceserver-jwt-validation-custom]]
|
|
|
==== Configuring a Custom Validator
|
|
|
|
|
|
Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API:
|
|
@@ -817,7 +792,7 @@ JwtDecoder jwtDecoder() {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-[[oauth2resourceserver-claimsetmapping]]
|
|
|
+[[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.
|
|
@@ -831,7 +806,7 @@ Or, quite simply, a resource server may want to add or remove claims from a JWT
|
|
|
|
|
|
For these purposes, Resource Server supports mapping the JWT claim set with `MappedJwtClaimSetConverter`.
|
|
|
|
|
|
-[[oauth2resourceserver-claimsetmapping-singleclaim]]
|
|
|
+[[oauth2resourceserver-jwt-claimsetmapping-singleclaim]]
|
|
|
==== Customizing the Conversion of a Single Claim
|
|
|
|
|
|
By default, `MappedJwtClaimSetConverter` will attempt to coerce claims into the following types:
|
|
@@ -852,7 +827,7 @@ An individual claim's conversion strategy can be configured using `MappedJwtClai
|
|
|
```java
|
|
|
@Bean
|
|
|
JwtDecoder jwtDecoder() {
|
|
|
- NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(JwtProcessors.withJwkSetUri(jwkSetUri).build());
|
|
|
+ NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
|
|
|
|
|
|
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
|
|
|
.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
|
|
@@ -863,7 +838,7 @@ JwtDecoder jwtDecoder() {
|
|
|
```
|
|
|
This will keep all the defaults, except it will override the default claim converter for `sub`.
|
|
|
|
|
|
-[[oauth2resourceserver-claimsetmapping-add]]
|
|
|
+[[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:
|
|
@@ -872,7 +847,7 @@ This will keep all the defaults, except it will override the default claim conve
|
|
|
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
|
|
|
```
|
|
|
|
|
|
-[[oauth2resourceserver-claimsetmapping-remove]]
|
|
|
+[[oauth2resourceserver-jwt-claimsetmapping-remove]]
|
|
|
==== Removing a Claim
|
|
|
|
|
|
And removing a claim is also simple, using the same API:
|
|
@@ -881,7 +856,7 @@ And removing a claim is also simple, using the same API:
|
|
|
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
|
|
|
```
|
|
|
|
|
|
-[[oauth2resourceserver-claimsetmapping-rename]]
|
|
|
+[[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<String, Object>, Map<String,Object>>`:
|
|
@@ -907,13 +882,13 @@ And then, the instance can be supplied like normal:
|
|
|
```java
|
|
|
@Bean
|
|
|
JwtDecoder jwtDecoder() {
|
|
|
- NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(JwtProcessors.withJwkSetUri(jwkSetUri).build());
|
|
|
+ NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
|
|
|
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
|
|
|
return jwtDecoder;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-[[oauth2resourceserver-timeouts]]
|
|
|
+[[oauth2resourceserver-jwt-timeouts]]
|
|
|
=== Configuring Timeouts
|
|
|
|
|
|
By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server.
|
|
@@ -931,11 +906,389 @@ public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
|
|
|
.setReadTimeout(60000)
|
|
|
.build();
|
|
|
|
|
|
- NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(JwtProcessors.withJwkSetUri(jwkSetUri).restOperations(rest).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:
|
|
|
+
|
|
|
+```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>>
|
|
|
+* <<oauth2resourceserver-opaque-authorization-extraction,Extracting Authorities Manually>>
|
|
|
+* <<oauth2resourceserver-opaque-jwt-introspector,Using Introspection with JWTs>>
|
|
|
+
|
|
|
+[[oauth2resourceserver-opaque-attributes]]
|
|
|
+=== Looking Up Attributes Post-Authentication
|
|
|
+
|
|
|
+Once a token is authenticated, an instance of `BearerTokenAuthentication` is set in the `SecurityContext`.
|
|
|
+
|
|
|
+This means that it's available in `@Controller` methods when using `@EnableWebMvc` in your configuration:
|
|
|
+
|
|
|
+```java
|
|
|
+@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:
|
|
|
+
|
|
|
+```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:
|
|
|
+
|
|
|
+```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:
|
|
|
+
|
|
|
+```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`:
|
|
|
+
|
|
|
+```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 <<oauth2resourceserver-opaque-introspectionuri,as a configuration property>> or it can be supplied in the DSL:
|
|
|
+
|
|
|
+```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`:
|
|
|
+
|
|
|
+```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 <<oauth2resourceserver-opaque-authorization-extraction,authority mapping>>, <<oauth2resourceserver-opaque-jwt-introspector,JWT revocation>>, or <<oauth2resourceserver-opaque-timeouts,request timeouts>>, is necessary.
|
|
|
+
|
|
|
+[[oauth2resourceserver-opaque-introspector-bean]]
|
|
|
+==== Exposing a `OpaqueTokenIntrospector` `@Bean`
|
|
|
+
|
|
|
+Or, exposing a `OpaqueTokenIntrospector` `@Bean` has the same effect as `introspector()`:
|
|
|
+
|
|
|
+```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<Message> 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:
|
|
|
+
|
|
|
+```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:
|
|
|
+
|
|
|
+```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<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
|
|
|
+ List<String> 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`:
|
|
|
+
|
|
|
+```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:
|
|
|
+
|
|
|
+```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:
|
|
|
+
|
|
|
+```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<SecurityContext> {
|
|
|
+ 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`:
|
|
|
+
|
|
|
+```java
|
|
|
+@Bean
|
|
|
+public OpaqueTokenIntrospector introspector() {
|
|
|
+ return new JwtOpaqueTokenIntropsector();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
[[jc-authentication]]
|
|
|
== Authentication
|
|
|
|