Align OAuth Reactive/Servlet Resource Server Docs
Fixes gh-7430 Fixes gh-7425 Fixes gh-7460
This commit is contained in:
parent
c1ae997adc
commit
f22fdf1bc0
|
@ -1,14 +1,17 @@
|
|||
[[webflux-oauth2-resource-server]]
|
||||
= OAuth2 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-webflux[*OAuth 2.0 Resource Server WebFlux sample*].
|
||||
A complete working example for {gh-samples-url}/boot/oauth2resourceserver-webflux[*JWTs*] is available in the {gh-samples-url}[Spring Security repository].
|
||||
====
|
||||
|
||||
== Dependencies
|
||||
|
@ -16,13 +19,13 @@ 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.
|
||||
|
||||
[[webflux-oauth2-resource-server-minimal-configuration]]
|
||||
== Minimal Configuration
|
||||
[[webflux-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.
|
||||
|
||||
=== Specify the Authorization Server
|
||||
=== Specifying the Authorization Server
|
||||
|
||||
In a Spring Boot application, to specify which authorization server to use, simply do:
|
||||
|
||||
|
@ -51,7 +54,7 @@ When this property and these dependencies are used, Resource Server will automat
|
|||
|
||||
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://idp.example.com`.
|
||||
|
||||
|
@ -72,7 +75,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
|
||||
|
@ -83,11 +86,13 @@ As the authorization server makes available new keys, Spring Security will autom
|
|||
|
||||
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,How to Configure without Tying Resource Server startup to an authorization server's availability>>
|
||||
From here, consider jumping to:
|
||||
|
||||
<<webflux-oauth2-resource-server-sans-boot,How to Configure without Spring Boot>>
|
||||
<<webflux-oauth2resourceserver-jwt-jwkseturi,How to Configure without Tying Resource Server startup to an authorization server's availability>>
|
||||
|
||||
[[webflux-oauth2-resource-server-jwkseturi]]
|
||||
<<webflux-oauth2resourceserver-jwt-sansboot,How to Configure without Spring Boot>>
|
||||
|
||||
[[webflux-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`:
|
||||
|
@ -109,14 +114,14 @@ 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-jwkseturi-dsl,DSL>>.
|
||||
This property can also be supplied directly on the <<webflux-oauth2resourceserver-jwt-jwkseturi-dsl,DSL>>.
|
||||
|
||||
[[webflux-oauth2-resource-server-sans-boot]]
|
||||
[[webflux-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 `SecurityWebFilterChain` that configures the app as a resource server:
|
||||
The first is a `SecurityWebFilterChain` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `WebSecurityConfigurerAdapter` looks like:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
@ -127,10 +132,7 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
|||
exchanges
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2ResourceServer ->
|
||||
oauth2ResourceServer
|
||||
.jwt(withDefaults())
|
||||
);
|
||||
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
@ -175,10 +177,10 @@ If the application doesn't expose a `ReactiveJwtDecoder` bean, then Spring Boot
|
|||
|
||||
And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`.
|
||||
|
||||
[[webflux-oauth2-resource-server-jwkseturi-dsl]]
|
||||
[[webflux-oauth2resourceserver-jwt-jwkseturi-dsl]]
|
||||
==== Using `jwkSetUri()`
|
||||
|
||||
An authorization server's JWK Set Uri can be configured <<webflux-oauth2-resource-server-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
|
||||
An authorization server's JWK Set Uri can be configured <<webflux-oauth2resourceserver-jwt-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
@ -202,7 +204,7 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
|||
|
||||
Using `jwkSetUri()` takes precedence over any configuration property.
|
||||
|
||||
[[webflux-oauth2-resource-server-decoder-dsl]]
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-dsl]]
|
||||
==== Using `decoder()`
|
||||
|
||||
More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`:
|
||||
|
@ -212,24 +214,19 @@ More powerful than `jwkSetUri()` is `decoder()`, which will completely replace a
|
|||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange(exchanges ->
|
||||
exchanges
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2ResourceServer ->
|
||||
oauth2ResourceServer
|
||||
.jwt(jwt ->
|
||||
jwt
|
||||
.decoder(myCustomDecoder())
|
||||
)
|
||||
);
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.decoder(myCustomDecoder());
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
This is handy when deeper configuration, like <<webflux-oauth2-resource-server-validation,validation>>, is necessary.
|
||||
This is handy when deeper configuration, like <<webflux-oauth2resourceserver-jwt-validation,validation>>, is necessary.
|
||||
|
||||
[[webflux-oauth2-resource-server-decoder-bean]]
|
||||
[[webflux-oauth2resourceserver-decoder-bean]]
|
||||
==== Exposing a `ReactiveJwtDecoder` `@Bean`
|
||||
|
||||
Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
|
||||
|
@ -237,12 +234,147 @@ Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
|
|||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return new NimbusReactiveJwtDecoder(jwkSetUri);
|
||||
public ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-authorization]]
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-algorithm]]
|
||||
== Configuring Trusted Algorithms
|
||||
|
||||
By default, `NimbusReactiveJwtDecoder`, and hence Resource Server, will only trust and verify tokens using `RS256`.
|
||||
|
||||
You can customize this via <<webflux-oauth2resourceserver-jwt-boot-algorithm,Spring Boot>> or <<webflux-oauth2resourceserver-jwt-decoder-builder,the NimbusJwtDecoder builder>>.
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-boot-algorithm]]
|
||||
==== Via Spring Boot
|
||||
|
||||
The simplest way to set the algorithm is as a property:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jws-algorithm: RS512
|
||||
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
|
||||
----
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-builder]]
|
||||
==== Using a Builder
|
||||
|
||||
For greater power, though, we can use a builder that ships with `NimbusReactiveJwtDecoder`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
|
||||
.jwsAlgorithm(RS512).build();
|
||||
}
|
||||
----
|
||||
|
||||
Calling `jwsAlgorithm` more than once will configure `NimbusReactiveJwtDecoder` to trust more than one algorithm, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(EC512).build();
|
||||
}
|
||||
----
|
||||
|
||||
Or, you can call `jwsAlgorithms`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
|
||||
.jwsAlgorithms(algorithms -> {
|
||||
algorithms.add(RS512);
|
||||
algorithms.add(EC512);
|
||||
}).build();
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-public-key]]
|
||||
=== Trusting a Single Asymmetric Key
|
||||
|
||||
Simpler than backing a Resource Server with a JWK Set endpoint is to hard-code an RSA public key.
|
||||
The public key can be provided via <<webflux-oauth2resourceserver-jwt-decoder-public-key-boot,Spring Boot>> or by <<webflux-oauth2resourceserver-jwt-decoder-public-key-builder,Using a Builder>>.
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-public-key-boot]]
|
||||
==== Via Spring Boot
|
||||
|
||||
Specifying a key via Spring Boot is quite simple.
|
||||
The key's location can be specified like so:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
public-key-location: classpath:my-key.pub
|
||||
----
|
||||
|
||||
Or, to allow for a more sophisticated lookup, you can post-process the `RsaKeyConversionServicePostProcessor`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
BeanFactoryPostProcessor conversionServiceCustomizer() {
|
||||
return beanFactory ->
|
||||
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
|
||||
.setResourceLoader(new CustomResourceLoader());
|
||||
}
|
||||
----
|
||||
|
||||
Specify your key's location:
|
||||
|
||||
```yaml
|
||||
key.location: hfds://my-key.pub
|
||||
```
|
||||
|
||||
And then autowire the value:
|
||||
|
||||
```java
|
||||
@Value("${key.location}")
|
||||
RSAPublicKey key;
|
||||
```
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-public-key-builder]]
|
||||
==== Using a Builder
|
||||
|
||||
To wire an `RSAPublicKey` directly, you can simply use the appropriate `NimbusReactiveJwtDecoder` builder, like so:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
|
||||
}
|
||||
```
|
||||
|
||||
[[webflux-oauth2resourceserver-jwt-decoder-secret-key]]
|
||||
=== Trusting a Single Symmetric Key
|
||||
|
||||
Using a single symmetric key is also simple.
|
||||
You can simply load in your `SecretKey` and use the appropriate `NimbusReactiveJwtDecoder` builder, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public ReactiveJwtDecoder jwtDecoder() {
|
||||
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-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:
|
||||
|
@ -258,16 +390,12 @@ This means that to protect an endpoint or method with a scope derived from a JWT
|
|||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange(exchanges ->
|
||||
exchanges
|
||||
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
|
||||
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
|
||||
.anyExchange().authenticated()
|
||||
.authorizeExchange(exchanges ->exchanges
|
||||
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
|
||||
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2ResourceServer ->
|
||||
oauth2ResourceServer
|
||||
.jwt(withDefaults())
|
||||
);
|
||||
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
@ -277,10 +405,10 @@ Or similarly with method security:
|
|||
[source,java]
|
||||
----
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
public List<Message> getMessages(...) {}
|
||||
public Flux<Message> getMessages(...) {}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-authorization-extraction]]
|
||||
[[webflux-oauth2resourceserver-jwt-authorization-extraction]]
|
||||
==== Extracting Authorities Manually
|
||||
|
||||
However, there are a number of circumstances where this default is insufficient.
|
||||
|
@ -294,34 +422,35 @@ To this end, the DSL exposes `jwtAuthenticationConverter()`:
|
|||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange(exchanges ->
|
||||
exchanges
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2ResourceServer ->
|
||||
oauth2ResourceServer
|
||||
.jwt(jwt ->
|
||||
jwt
|
||||
.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
|
||||
)
|
||||
);
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.jwtAuthenticationConverter(grantedAuthoritiesExtractor());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
|
||||
GrantedAuthoritiesExtractor extractor = new GrantedAuthoritiesExtractor();
|
||||
return new ReactiveJwtAuthenticationConverterAdapter(extractor);
|
||||
JwtAuthenticationConverter jwtAuthenticationConverter =
|
||||
new JwtAuthenticationConverter();
|
||||
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
|
||||
(new GrantedAuthoritiesExtractor());
|
||||
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
|
||||
}
|
||||
----
|
||||
|
||||
which is responsible for converting a `Jwt` into an `Authentication`.
|
||||
As part of its configuration, we can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities.
|
||||
|
||||
We can override this quite simply to alter the way granted authorities are derived:
|
||||
That final converter might be something like `GrantedAuthoritiesExtractor` below:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter {
|
||||
protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
|
||||
static class GrantedAuthoritiesExtractor
|
||||
implements Converter<Jwt, Collection<GrantedAuthority>> {
|
||||
|
||||
public Collection<GrantedAuthority> convert(Jwt jwt) {
|
||||
Collection<String> authorities = (Collection<String>)
|
||||
jwt.getClaims().get("mycustomclaim");
|
||||
|
||||
|
@ -343,14 +472,14 @@ static class CustomAuthenticationConverter implements Converter<Jwt, Mono<Abstra
|
|||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-validation]]
|
||||
[[webflux-oauth2resourceserver-jwt-validation]]
|
||||
=== Configuring Validation
|
||||
|
||||
Using <<webflux-oauth2-resource-server-minimal-configuration,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 <<webflux-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.
|
||||
|
||||
[[webflux-oauth2-resource-server-validation-clockskew]]
|
||||
[[webflux-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.
|
||||
|
@ -380,7 +509,7 @@ ReactiveJwtDecoder jwtDecoder() {
|
|||
[NOTE]
|
||||
By default, Resource Server configures a clock skew of 30 seconds.
|
||||
|
||||
[[webflux-oauth2-resource-server-validation-custom]]
|
||||
[[webflux-oauth2resourceserver-validation-custom]]
|
||||
==== Configuring a Custom Validator
|
||||
|
||||
Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API:
|
||||
|
@ -419,3 +548,452 @@ ReactiveJwtDecoder jwtDecoder() {
|
|||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-minimalconfiguration]]
|
||||
=== Minimal Configuration for Introspection
|
||||
|
||||
Typically, an opaque token can be verified via an https://tools.ietf.org/html/rfc7662[OAuth 2.0 Introspection Endpoint], hosted by the authorization server.
|
||||
This can be handy when revocation is a requirement.
|
||||
|
||||
When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a resource server that uses introspection consists of two basic steps.
|
||||
First, include the needed dependencies and second, indicate the introspection endpoint details.
|
||||
|
||||
==== Specifying the Authorization Server
|
||||
|
||||
To specify where the introspection endpoint is, simply do:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
opaque-token:
|
||||
introspection-uri: https://idp.example.com/introspect
|
||||
client-id: client
|
||||
client-secret: secret
|
||||
----
|
||||
|
||||
Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint.
|
||||
|
||||
Resource Server will use these properties to further self-configure and subsequently validate incoming JWTs.
|
||||
|
||||
[NOTE]
|
||||
When using introspection, the authorization server's word is the law.
|
||||
If the authorization server responses that the token is valid, then it is.
|
||||
|
||||
And that's it!
|
||||
|
||||
==== Startup Expectations
|
||||
|
||||
When this property and these dependencies are used, Resource Server will automatically configure itself to validate Opaque Bearer Tokens.
|
||||
|
||||
This startup process is quite a bit simpler than for JWTs since no endpoints need to be discovered and no additional validation rules get added.
|
||||
|
||||
==== Runtime Expectations
|
||||
|
||||
Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
|
||||
|
||||
```http
|
||||
GET / HTTP/1.1
|
||||
Authorization: Bearer some-token-value # Resource Server will process this
|
||||
```
|
||||
|
||||
So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
|
||||
|
||||
Given an Opaque Token, Resource Server will
|
||||
|
||||
1. Query the provided introspection endpoint using the provided credentials and the token
|
||||
2. Inspect the response for an `{ 'active' : true }` attribute
|
||||
3. Map each scope to an authority with the prefix `SCOPE_`
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `OAuth2AuthenticatedPrincipal` object, and `Authentication#getName` maps to the token's `sub` property, if one is present.
|
||||
|
||||
From here, you may want to jump to:
|
||||
|
||||
* <<webflux-oauth2resourceserver-opaque-attributes,Looking Up Attributes Post-Authentication>>
|
||||
* <<webflux-oauth2resourceserver-opaque-authorization-extraction,Extracting Authorities Manually>>
|
||||
* <<webflux-oauth2resourceserver-opaque-jwt-introspector,Using Introspection with JWTs>>
|
||||
|
||||
[[webflux-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 `@EnableWebFlux` in your configuration:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@GetMapping("/foo")
|
||||
public Mono<String> foo(BearerTokenAuthentication authentication) {
|
||||
return Mono.just(authentication.getTokenAttributes().get("sub") + " is the subject");
|
||||
}
|
||||
----
|
||||
|
||||
Since `BearerTokenAuthentication` holds an `OAuth2AuthenticatedPrincipal`, that also means that it's available to controller methods, too:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@GetMapping("/foo")
|
||||
public Mono<String> foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
|
||||
return Mono.just(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 `@EnableReactiveMethodSecurity` so that you can use `@PreAuthorize` annotations, you can do:
|
||||
|
||||
```java
|
||||
@PreAuthorize("principal?.attributes['sub'] == 'foo'")
|
||||
public Mono<String> forFoosEyesOnly() {
|
||||
return Mono.just("foo");
|
||||
}
|
||||
```
|
||||
|
||||
[[webflux-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 `SecurityWebFilterChain` that configures the app as a resource server.
|
||||
When use Opaque Token, this `SecurityWebFilterChain` looks like:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken)
|
||||
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]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
public class MyCustomSecurityConfiguration {
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.pathMatchers("/messages/**").hasAuthority("SCOPE_message:read")
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.opaqueToken()
|
||||
.introspector(myIntrospector());
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
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 `ReactiveOpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public ReactiveOpaqueTokenIntrospector introspector() {
|
||||
return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
|
||||
}
|
||||
----
|
||||
|
||||
If the application doesn't expose a `ReactiveOpaqueTokenIntrospector` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`.
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-introspectionuri-dsl]]
|
||||
==== Using `introspectionUri()`
|
||||
|
||||
An authorization server's Introspection Uri can be configured <<webflux-oauth2resourceserver-opaque-introspectionuri,as a configuration property>> or it can be supplied in the DSL:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
public class DirectlyConfiguredIntrospectionUri {
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.opaqueToken()
|
||||
.introspectionUri("https://idp.example.com/introspect")
|
||||
.introspectionClientCredentials("client", "secret");
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Using `introspectionUri()` takes precedence over any configuration property.
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-introspector-dsl]]
|
||||
==== Using `introspector()`
|
||||
|
||||
More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of `ReactiveOpaqueTokenIntrospector`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
public class DirectlyConfiguredIntrospector {
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.opaqueToken()
|
||||
.introspector(myCustomIntrospector());
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This is handy when deeper configuration, like <<webflux-oauth2resourceserver-opaque-authorization-extraction,authority mapping>>or <<webflux-oauth2resourceserver-opaque-jwt-introspector,JWT revocation>> is necessary.
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-introspector-bean]]
|
||||
==== Exposing a `ReactiveOpaqueTokenIntrospector` `@Bean`
|
||||
|
||||
Or, exposing a `ReactiveOpaqueTokenIntrospector` `@Bean` has the same effect as `introspector()`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public ReactiveOpaqueTokenIntrospector introspector() {
|
||||
return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-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
|
||||
@EnableWebFluxSecurity
|
||||
public class MappedAuthorities {
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange(exchange -> exchange
|
||||
.pathMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
|
||||
.pathMatchers("/messages/**").hasAuthority("SCOPE_messages")
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or similarly with method security:
|
||||
|
||||
```java
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
public Flux<Message> getMessages(...) {}
|
||||
```
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-authorization-extraction]]
|
||||
==== Extracting Authorities Manually
|
||||
|
||||
By default, Opaque Token support will extract the scope claim from an introspection response and parse it into individual `GrantedAuthority` instances.
|
||||
|
||||
For example, if the introspection response were:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"active" : true,
|
||||
"scope" : "message:read message:write"
|
||||
}
|
||||
----
|
||||
|
||||
Then Resource Server would generate an `Authentication` with two authorities, one for `message:read` and the other for `message:write`.
|
||||
|
||||
This can, of course, be customized using a custom `ReactiveOpaqueTokenIntrospector` that takes a look at the attribute set and converts in its own way:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
|
||||
private ReactiveOpaqueTokenIntrospector delegate =
|
||||
new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
|
||||
|
||||
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
|
||||
return this.delegate.introspect(token)
|
||||
.map(principal -> 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`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public ReactiveOpaqueTokenIntrospector introspector() {
|
||||
return new CustomAuthoritiesOpaqueTokenIntrospector();
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-jwt-introspector]]
|
||||
=== Using Introspection with JWTs
|
||||
|
||||
A common question is whether or not introspection is compatible with JWTs.
|
||||
Spring Security's Opaque Token support has been designed to not care about the format of the token -- it will gladly pass any token to the introspection endpoint provided.
|
||||
|
||||
So, let's say that you've got a requirement that requires you to check with the authorization server on each request, in case the JWT has been revoked.
|
||||
|
||||
Even though you are using the JWT format for the token, your validation method is introspection, meaning you'd want to do:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
opaque-token:
|
||||
introspection-uri: https://idp.example.org/introspection
|
||||
client-id: client
|
||||
client-secret: secret
|
||||
----
|
||||
|
||||
In this case, the resulting `Authentication` would be `BearerTokenAuthentication`.
|
||||
Any attributes in the corresponding `OAuth2AuthenticatedPrincipal` would be whatever was returned by the introspection endpoint.
|
||||
|
||||
But, let's say that, oddly enough, the introspection endpoint only returns whether or not the token is active.
|
||||
Now what?
|
||||
|
||||
In this case, you can create a custom `ReactiveOpaqueTokenIntrospector` that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
|
||||
private ReactiveOpaqueTokenIntrospector delegate =
|
||||
new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
|
||||
private ReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(new ParseOnlyJWTProcessor());
|
||||
|
||||
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
|
||||
return this.delegate.introspect(token)
|
||||
.flatMap(principal -> this.jwtDecoder.decode(token))
|
||||
.map(jwt -> new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES));
|
||||
}
|
||||
|
||||
private static class ParseOnlyJWTProcessor implements Converter<JWT, Mono<JWTClaimsSet>> {
|
||||
public Mono<JWTClaimsSet> convert(JWT jwt) {
|
||||
try {
|
||||
return Mono.just(jwt.getJWTClaimsSet());
|
||||
} catch (Exception e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public ReactiveOpaqueTokenIntrospector introspector() {
|
||||
return new JwtOpaqueTokenIntropsector();
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2resourceserver-opaque-userinfo]]
|
||||
=== Calling a `/userinfo` Endpoint
|
||||
|
||||
Generally speaking, a Resource Server doesn't care about the underlying user, but instead about the authorities that have been granted.
|
||||
|
||||
That said, at times it can be valuable to tie the authorization statement back to a user.
|
||||
|
||||
If an application is also using `spring-security-oauth2-client`, having set up the appropriate `ClientRegistrationRepository`, then this is quite simple with a custom `OpaqueTokenIntrospector`.
|
||||
This implementation below does three things:
|
||||
|
||||
* Delegates to the introspection endpoint, to affirm the token's validity
|
||||
* Looks up the appropriate client registration associated with the `/userinfo` endpoint
|
||||
* Invokes and returns the response from the `/userinfo` endpoint
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
|
||||
private final ReactiveOpaqueTokenIntrospector delegate =
|
||||
new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
|
||||
private final ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService =
|
||||
new DefaultReactiveOAuth2UserService();
|
||||
|
||||
private final ReactiveClientRegistrationRepository repository;
|
||||
|
||||
// ... constructor
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
|
||||
return Mono.zip(this.delegate.introspect(token), this.repository.findByRegistrationId("registration-id"))
|
||||
.map(t -> {
|
||||
OAuth2AuthenticatedPrincipal authorized = t.getT1();
|
||||
ClientRegistration clientRegistration = t.getT2();
|
||||
Instant issuedAt = authorized.getAttribute(ISSUED_AT);
|
||||
Instant expiresAt = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT);
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
|
||||
return new OAuth2UserRequest(clientRegistration, accessToken);
|
||||
})
|
||||
.flatMap(this.oauth2UserService::loadUser);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
If you aren't using `spring-security-oauth2-client`, it's still quite simple.
|
||||
You will simply need to invoke the `/userinfo` with your own instance of `WebClient`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
|
||||
private final ReactiveOpaqueTokenIntrospector delegate =
|
||||
new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
|
||||
private final WebClient rest = WebClient.create();
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
|
||||
return this.delegate.introspect(token)
|
||||
.map(this::makeUserInfoRequest);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Either way, having created your `ReactiveOpaqueTokenIntrospector`, you should publish it as a `@Bean` to override the defaults:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ReactiveOpaqueTokenIntrospector introspector() {
|
||||
return new UserInfoOpaqueTokenIntrospector(...);
|
||||
}
|
||||
----
|
||||
|
||||
|
|
|
@ -148,3 +148,123 @@ this.rest
|
|||
.uri("/login")
|
||||
...
|
||||
----
|
||||
|
||||
|
||||
=== Testing Bearer Authentication
|
||||
|
||||
In order to make an authorized request on a resource server, you need a bearer token.
|
||||
If your resource server is configured for JWTs, then this would mean that the bearer token needs to be signed and then encoded according to the JWT specification.
|
||||
All of this can be quite daunting, especially when this isn't the focus of your test.
|
||||
|
||||
Fortunately, there are a number of simple ways that you can overcome this difficulty and allow your tests to focus on authorization and not on representing bearer tokens.
|
||||
We'll look at two of them now:
|
||||
|
||||
===== `mockJwt() WebTestClientConfigurer`
|
||||
|
||||
The first way is via a `WebTestClientConfigurer`.
|
||||
The simplest of these would look something like this:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
client
|
||||
.mutateWith(mockJwt()).get().uri("/endpoint").exchange();
|
||||
----
|
||||
|
||||
What this will do is create a mock `Jwt`, passing it correctly through any authentication APIs so that it's available for your authorization mechanisms to verify.
|
||||
|
||||
By default, the `JWT` that it creates has the following characteristics:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"headers" : { "alg" : "none" },
|
||||
"claims" : {
|
||||
"sub" : "user",
|
||||
"scope" : "read"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
And the resulting `Jwt`, were it tested, would pass in the following way:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
assertThat(jwt.getTokenValue()).isEqualTo("token");
|
||||
assertThat(jwt.getHeaders().get("alg")).isEqualTo("none");
|
||||
assertThat(jwt.getSubject()).isEqualTo("sub");
|
||||
GrantedAuthority authority = jwt.getAuthorities().iterator().next();
|
||||
assertThat(authority.getAuthority()).isEqualTo("read");
|
||||
----
|
||||
|
||||
These values can, of course be configured.
|
||||
|
||||
Any headers or claims can be configured with their corresponding methods:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
client
|
||||
.mutateWith(jwt(jwt -> jwt.header("kid", "one")
|
||||
.claim("iss", "https://idp.example.org")))
|
||||
.get().uri("/endpoint").exchange();
|
||||
----
|
||||
|
||||
[source,java]
|
||||
----
|
||||
client
|
||||
.mutateWith(jwt(jwt -> jwt.claims(claims -> claims.remove("scope"))))
|
||||
.get().uri("/endpoint").exchange();
|
||||
----
|
||||
|
||||
The `scope` and `scp` claims are processed the same way here as they are in a normal bearer token request.
|
||||
However, this can be overridden simply by providing the list of `GrantedAuthority` instances that you need for your test:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
client
|
||||
.mutateWith(jwt().authorities(new SimpleGrantedAuthority("SCOPE_messages")))
|
||||
.get().uri("/endpoint").exchange();
|
||||
----
|
||||
|
||||
Or, if you have a custom `Jwt` to `Collection<GrantedAuthority>` converter, you can also use that to derive the authorities:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
client
|
||||
.mutateWith(jwt().authorities(new MyConverter()))
|
||||
.get().uri("/endpoint").exchange();
|
||||
----
|
||||
|
||||
You can also specify a complete `Jwt`, for which `Jwt.Builder` comes quite handy:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Jwt jwt = Jwt.withTokenValue("token")
|
||||
.header("alg", "none")
|
||||
.claim("sub", "user")
|
||||
.claim("scope", "read");
|
||||
|
||||
client
|
||||
.mutateWith(jwt(jwt))
|
||||
.get().uri("/endpoint").exchange();
|
||||
----
|
||||
|
||||
===== `authentication()` `WebTestClientConfigurer`
|
||||
|
||||
The second way is by using the `authentication()` `Mutator`.
|
||||
Essentially, you can instantiate your own `JwtAuthenticationToken` and provide it in your test, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Jwt jwt = Jwt.withTokenValue("token")
|
||||
.header("alg", "none")
|
||||
.claim("sub", "user")
|
||||
.build();
|
||||
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("SCOPE_read");
|
||||
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
|
||||
|
||||
client
|
||||
.mutateWith(authentication(token))
|
||||
.get().uri("/endpoint").exchange();
|
||||
----
|
||||
|
||||
Note that as an alternative to these, you can also mock the `ReactiveJwtDecoder` bean itself with a `@MockBean` annotation.
|
||||
|
|
|
@ -430,15 +430,17 @@ First, include the needed dependencies and second, indicate the location of the
|
|||
|
||||
==== Specifying the Authorization Server
|
||||
|
||||
To specify which authorization server to use, simply do:
|
||||
In a Spring Boot application, to specify which authorization server to use, simply do:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: https://idp.example.com
|
||||
```
|
||||
[source,yml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: https://idp.example.com
|
||||
----
|
||||
|
||||
Where `https://idp.example.com` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
|
||||
Resource Server will use this property to further self-configure, discover the authorization server's public keys, and subsequently validate incoming JWTs.
|
||||
|
@ -468,14 +470,15 @@ If the authorization server is down when Resource Server queries it (given appro
|
|||
|
||||
Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
|
||||
|
||||
```http
|
||||
[source,html]
|
||||
----
|
||||
GET / HTTP/1.1
|
||||
Authorization: Bearer some-token-value # Resource Server will process this
|
||||
```
|
||||
----
|
||||
|
||||
So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
|
||||
|
||||
Given a well-formed JWT, Resource Server will
|
||||
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
|
||||
|
@ -497,13 +500,15 @@ From here, consider jumping to:
|
|||
|
||||
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://idp.example.com/.well-known/jwks.json
|
||||
```
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
The JWK Set uri is not standardized, but can typically be found in the authorization server's documentation
|
||||
|
@ -521,7 +526,8 @@ There are two `@Bean` s that Spring Boot generates on Resource Server's behalf.
|
|||
|
||||
The first is a `WebSecurityConfigurerAdapter` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `WebSecurityConfigurerAdapter` looks like:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
protected void configure(HttpSecurity http) {
|
||||
http
|
||||
.authorizeRequests()
|
||||
|
@ -529,13 +535,14 @@ protected void configure(HttpSecurity http) {
|
|||
.and()
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
Replacing this is as simple as exposing the bean within the application:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) {
|
||||
|
@ -549,7 +556,7 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter
|
|||
.jwtAuthenticationConverter(myConverter());
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
The above requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
|
||||
|
@ -557,12 +564,13 @@ Methods on the `oauth2ResourceServer` DSL will also override or replace auto con
|
|||
|
||||
For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return JwtDecoders.fromOidcIssuerLocation(issuerUri);
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
If the application doesn't expose a `JwtDecoder` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
|
@ -573,7 +581,8 @@ And its configuration can be overridden using `jwkSetUri()` or replaced using `d
|
|||
|
||||
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
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) {
|
||||
|
@ -586,7 +595,7 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
|||
.jwkSetUri("https://idp.example.com/.well-known/jwks.json");
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Using `jwkSetUri()` takes precedence over any configuration property.
|
||||
|
||||
|
@ -595,7 +604,8 @@ Using `jwkSetUri()` takes precedence over any configuration property.
|
|||
|
||||
More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) {
|
||||
|
@ -608,7 +618,7 @@ public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter {
|
|||
.decoder(myCustomDecoder());
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
This is handy when deeper configuration, like <<oauth2resourceserver-jwt-validation,validation>>, <<oauth2resourceserver-jwt-claimsetmapping,mapping>>, or <<oauth2resourceserver-jwt-timeouts,request timeouts>>, is necessary.
|
||||
|
||||
|
@ -617,26 +627,28 @@ This is handy when deeper configuration, like <<oauth2resourceserver-jwt-validat
|
|||
|
||||
Or, exposing a `JwtDecoder` `@Bean` has the same effect as `decoder()`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2resourceserver-jwt-decoder-algorithm]]
|
||||
=== Configuring Trusted Algorithms
|
||||
|
||||
By default, `NimbusJwtDecoder`, and hence Resource Server, will only trust and verify tokens using `RS256`.
|
||||
|
||||
You can customize this via <<oauth2-resourceserver-jwt-boot-algorithm,Spring Boot>>, <<oauth2-resourceserver-jwt-decoder-builder,the NimbusJwtDecoder builder>>, or from the <<oauth2-resourceserver-jwt-decoder-jwk-response,JWK Set response>>.
|
||||
You can customize this via <<oauth2resourceserver-jwt-boot-algorithm,Spring Boot>>, <<oauth2resourceserver-jwt-decoder-builder,the NimbusJwtDecoder builder>>, or from the <<oauth2resourceserver-jwt-decoder-jwk-response,JWK Set response>>.
|
||||
|
||||
[[oauth2-resourceserver-jwt-boot-algorithm]]
|
||||
[[oauth2resourceserver-jwt-boot-algorithm]]
|
||||
==== Via Spring Boot
|
||||
|
||||
The simplest way to set the algorithm is as a property:
|
||||
|
||||
```yaml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
|
@ -644,34 +656,37 @@ spring:
|
|||
jwt:
|
||||
jws-algorithm: RS512
|
||||
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2-resourceserver-jwt-decoder-builder]]
|
||||
[[oauth2resourceserver-jwt-decoder-builder]]
|
||||
==== Using a Builder
|
||||
|
||||
For greater power, though, we can use a builder that ships with `NimbusJwtDecoder`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
|
||||
.jwsAlgorithm(RS512).build();
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Calling `jwsAlgorithm` more than once will configure `NimbusJwtDecoder` to trust more than one algorithm, like so:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
|
||||
.jwsAlgorithm(RS512).jwsAlgorithm(EC512).build();
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Or, you can call `jwsAlgorithms`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
|
||||
|
@ -680,9 +695,9 @@ JwtDecoder jwtDecoder() {
|
|||
algorithms.add(EC512);
|
||||
}).build();
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2-resourceserver-jwt-decoder-jwk-response]]
|
||||
[[oauth2resourceserver-jwt-decoder-jwk-response]]
|
||||
==== From JWK Set response
|
||||
|
||||
Since Spring Security's JWT support is based off of Nimbus, you can use all it's great features as well.
|
||||
|
@ -717,25 +732,27 @@ The public key can be provided via <<oauth2resourceserver-jwt-decoder-public-key
|
|||
Specifying a key via Spring Boot is quite simple.
|
||||
The key's location can be specified like so:
|
||||
|
||||
```yaml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
public-key-location: classpath:my-key.pub
|
||||
```
|
||||
----
|
||||
|
||||
Or, to allow for a more sophisticated lookup, you can post-process the `RsaKeyConversionServicePostProcessor`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
BeanFactoryPostProcessor conversionServiceCustomizer() {
|
||||
return beanFactory ->
|
||||
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
|
||||
.setResourceLoader(new CustomResourceLoader());
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Specify your key's location:
|
||||
|
||||
|
@ -768,12 +785,13 @@ public JwtDecoder jwtDecoder() {
|
|||
Using a single symmetric key is also simple.
|
||||
You can simply load in your `SecretKey` and use the appropriate `NimbusJwtDecoder` builder, like so:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withSecretKey(this.key).build();
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2resourceserver-jwt-authorization]]
|
||||
=== Configuring Authorization
|
||||
|
@ -786,7 +804,8 @@ When this is the case, Resource Server will attempt to coerce these scopes into
|
|||
|
||||
This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) {
|
||||
|
@ -799,14 +818,15 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
|||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Or similarly with method security:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
public List<Message> getMessages(...) {}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2resourceserver-jwt-authorization-extraction]]
|
||||
==== Extracting Authorities Manually
|
||||
|
@ -817,7 +837,8 @@ Or, at other times, the resource server may need to adapt the attribute or a com
|
|||
|
||||
To this end, the DSL exposes `jwtAuthenticationConverter()`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) {
|
||||
|
@ -838,14 +859,15 @@ Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
|
|||
(new GrantedAuthoritiesExtractor());
|
||||
return jwtAuthenticationConveter;
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
which is responsible for converting a `Jwt` into an `Authentication`.
|
||||
As part of its configuration, we can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities.
|
||||
|
||||
That final converter might be something like `GrantedAuthoritiesExtractor` below:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
static class GrantedAuthoritiesExtractor
|
||||
implements Converter<Jwt, Collection<GrantedAuthority>> {
|
||||
|
||||
|
@ -858,17 +880,18 @@ static class GrantedAuthoritiesExtractor
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter<Jwt, AbstractAuthenticationToken>`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
|
||||
public AbstractAuthenticationToken convert(Jwt jwt) {
|
||||
return new CustomAuthenticationToken(jwt);
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2resourceserver-jwt-validation]]
|
||||
=== Configuring Validation
|
||||
|
@ -887,7 +910,8 @@ This can cause some implementation heartburn as the number of collaborating serv
|
|||
|
||||
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
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
|
||||
|
@ -901,7 +925,7 @@ JwtDecoder jwtDecoder() {
|
|||
|
||||
return jwtDecoder;
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
By default, Resource Server configures a clock skew of 30 seconds.
|
||||
|
@ -911,7 +935,8 @@ By default, Resource Server configures a clock skew of 30 seconds.
|
|||
|
||||
Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
|
||||
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
|
||||
|
||||
|
@ -923,11 +948,12 @@ public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
|
|||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Then, to add into a resource server, it's a matter of specifying the `JwtDecoder` instance:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
|
||||
|
@ -941,7 +967,7 @@ JwtDecoder jwtDecoder() {
|
|||
|
||||
return jwtDecoder;
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2resourceserver-jwt-claimsetmapping]]
|
||||
=== Configuring Claim Set Mapping
|
||||
|
@ -1075,7 +1101,8 @@ First, include the needed dependencies and second, indicate the introspection en
|
|||
|
||||
To specify where the introspection endpoint is, simply do:
|
||||
|
||||
```yaml
|
||||
[source,yaml]
|
||||
----
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
|
@ -1083,7 +1110,7 @@ security:
|
|||
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.
|
||||
|
||||
|
@ -1133,21 +1160,23 @@ Once a token is authenticated, an instance of `BearerTokenAuthentication` is set
|
|||
|
||||
This means that it's available in `@Controller` methods when using `@EnableWebMvc` in your configuration:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@GetMapping("/foo")
|
||||
public String foo(BearerTokenAuthentication authentication) {
|
||||
return authentication.getTokenAttributes().get("sub") + " is the subject";
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Since `BearerTokenAuthentication` holds an `OAuth2AuthenticatedPrincipal`, that also means that it's available to controller methods, too:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@GetMapping("/foo")
|
||||
public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
|
||||
return principal.getAttribute("sub") + " is the subject";
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
==== Looking Up Attributes Via SpEL
|
||||
|
||||
|
@ -1170,7 +1199,8 @@ 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
|
||||
[source,java]
|
||||
----
|
||||
protected void configure(HttpSecurity http) {
|
||||
http
|
||||
.authorizeRequests()
|
||||
|
@ -1178,13 +1208,14 @@ protected void configure(HttpSecurity http) {
|
|||
.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
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) {
|
||||
|
@ -1198,7 +1229,7 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter
|
|||
.introspector(myIntrospector());
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
The above requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
|
||||
|
@ -1206,12 +1237,13 @@ Methods on the `oauth2ResourceServer` DSL will also override or replace auto con
|
|||
|
||||
For example, the second `@Bean` Spring Boot creates is an `OpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public OpaqueTokenIntrospector introspector() {
|
||||
return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
If the application doesn't expose a `OpaqueTokenIntrospector` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
|
@ -1222,7 +1254,8 @@ And its configuration can be overridden using `introspectionUri()` and `introspe
|
|||
|
||||
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
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) {
|
||||
|
@ -1236,7 +1269,7 @@ public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAda
|
|||
.introspectionClientCredentials("client", "secret");
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Using `introspectionUri()` takes precedence over any configuration property.
|
||||
|
||||
|
@ -1245,7 +1278,8 @@ Using `introspectionUri()` takes precedence over any configuration property.
|
|||
|
||||
More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of `OpaqueTokenIntrospector`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) {
|
||||
|
@ -1258,7 +1292,7 @@ public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter
|
|||
.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.
|
||||
|
||||
|
@ -1267,12 +1301,13 @@ This is handy when deeper configuration, like <<oauth2resourceserver-opaque-auth
|
|||
|
||||
Or, exposing a `OpaqueTokenIntrospector` `@Bean` has the same effect as `introspector()`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public OpaqueTokenIntrospector introspector() {
|
||||
return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2resourceserver-opaque-authorization]]
|
||||
=== Configuring Authorization
|
||||
|
@ -1314,18 +1349,20 @@ By default, Opaque Token support will extract the scope claim from an introspect
|
|||
|
||||
For example, if the introspection response were:
|
||||
|
||||
```json
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"active" : true,
|
||||
"scope" : "message:read message:write"
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Then Resource Server would generate an `Authentication` with two authorities, one for `message:read` and the other for `message:write`.
|
||||
|
||||
This can, of course, be customized using a custom `OpaqueTokenIntrospector` that takes a look at the attribute set and converts in its own way:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
private OpaqueTokenIntrospector delegate =
|
||||
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
|
||||
|
@ -1343,16 +1380,17 @@ public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntr
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public OpaqueTokenIntrospector introspector() {
|
||||
return new CustomAuthoritiesOpaqueTokenIntrospector();
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2resourceserver-opaque-timeouts]]
|
||||
=== Configuring Timeouts
|
||||
|
@ -1387,7 +1425,8 @@ So, let's say that you've got a requirement that requires you to check with the
|
|||
|
||||
Even though you are using the JWT format for the token, your validation method is introspection, meaning you'd want to do:
|
||||
|
||||
```yaml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
|
@ -1396,7 +1435,7 @@ spring:
|
|||
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.
|
||||
|
@ -1406,7 +1445,8 @@ 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
|
||||
[source,java]
|
||||
----
|
||||
public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
private OpaqueTokenIntrospector delegate =
|
||||
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
|
||||
|
@ -1429,16 +1469,17 @@ public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public OpaqueTokenIntrospector introspector() {
|
||||
return new JwtOpaqueTokenIntropsector();
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
[[oauth2resourceserver-opaque-userinfo]]
|
||||
=== Calling a `/userinfo` Endpoint
|
||||
|
@ -1454,7 +1495,8 @@ This implementation below does three things:
|
|||
* Looks up the appropriate client registration associated with the `/userinfo` endpoint
|
||||
* Invokes and returns the response from the `/userinfo` endpoint
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
private final OpaqueTokenIntrospector delegate =
|
||||
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
|
||||
|
@ -1475,12 +1517,13 @@ public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector
|
|||
return this.oauth2UserService.loadUser(oauth2UserRequest);
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
If you aren't using `spring-security-oauth2-client`, it's still quite simple.
|
||||
You will simply need to invoke the `/userinfo` with your own instance of `WebClient`:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
private final OpaqueTokenIntrospector delegate =
|
||||
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
|
||||
|
@ -1492,19 +1535,17 @@ public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector
|
|||
return makeUserInfoRequest(authorized);
|
||||
}
|
||||
}
|
||||
```
|
||||
----
|
||||
|
||||
Either way, having created your `OpaqueTokenIntrospector`, you should publish it as a `@Bean` to override the defaults:
|
||||
|
||||
```java
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
OpaqueTokenIntrospector introspector() {
|
||||
return new UserInfoOpaqueTokenIntrospector(...);
|
||||
}
|
||||
```
|
||||
|
||||
[[jc-authentication]]
|
||||
== Authentication
|
||||
----
|
||||
|
||||
Thus far we have only taken a look at the most basic authentication configuration.
|
||||
Let's take a look at a few slightly more advanced options for configuring authentication.
|
||||
|
|
Loading…
Reference in New Issue