parent
dcdeab596d
commit
c85358915a
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue