mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-28 13:18:45 +00:00 
			
		
		
		
	
		
			
	
	
		
			883 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
		
		
			
		
	
	
			883 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
|  | = OAuth 2.0 Resource Server JWT | ||
|  | 
 | ||
|  | [[webflux-oauth2resourceserver-jwt-minimaldependencies]] | ||
|  | == Minimal Dependencies for JWT | ||
|  | 
 | ||
|  | 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-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. | ||
|  | 
 | ||
|  | === Specifying the Authorization Server | ||
|  | 
 | ||
|  | In a Spring Boot application, to specify which authorization server to use, simply do: | ||
|  | 
 | ||
|  | [source,yml] | ||
|  | ---- | ||
|  | spring: | ||
|  |   security: | ||
|  |     oauth2: | ||
|  |       resourceserver: | ||
|  |         jwt: | ||
|  |           issuer-uri: https://idp.example.com/issuer | ||
|  | ---- | ||
|  | 
 | ||
|  | Where `https://idp.example.com/issuer` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue. | ||
|  | Resource Server will use this property to further self-configure, discover the authorization server's public keys, and subsequently validate incoming JWTs. | ||
|  | 
 | ||
|  | [NOTE] | ||
|  | To use the `issuer-uri` property, it must also be true that one of `https://idp.example.com/issuer/.well-known/openid-configuration`, `https://idp.example.com/.well-known/openid-configuration/issuer`, or `https://idp.example.com/.well-known/oauth-authorization-server/issuer` is a supported endpoint for the authorization server. | ||
|  | This endpoint is referred to as a https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Provider Configuration] endpoint or a https://tools.ietf.org/html/rfc8414#section-3[Authorization Server Metadata] endpoint. | ||
|  | 
 | ||
|  | And that's it! | ||
|  | 
 | ||
|  | === Startup Expectations | ||
|  | 
 | ||
|  | When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens. | ||
|  | 
 | ||
|  | It achieves this through a deterministic startup process: | ||
|  | 
 | ||
|  | 1. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property | ||
|  | 2. Configure the validation strategy to query `jwks_url` for valid public keys | ||
|  | 3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`. | ||
|  | 
 | ||
|  | A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up. | ||
|  | 
 | ||
|  | [NOTE] | ||
|  | If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail. | ||
|  | 
 | ||
|  | === Runtime Expectations | ||
|  | 
 | ||
|  | Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header: | ||
|  | 
 | ||
|  | [source,html] | ||
|  | ---- | ||
|  | GET / HTTP/1.1 | ||
|  | Authorization: Bearer some-token-value # Resource Server will process this | ||
|  | ---- | ||
|  | 
 | ||
|  | So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification. | ||
|  | 
 | ||
|  | Given a well-formed JWT, Resource Server will: | ||
|  | 
 | ||
|  | 1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header | ||
|  | 2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and | ||
|  | 3. Map each scope to an authority with the prefix `SCOPE_`. | ||
|  | 
 | ||
|  | [NOTE] | ||
|  | As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens. | ||
|  | 
 | ||
|  | The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present. | ||
|  | 
 | ||
|  | From here, consider jumping to: | ||
|  | 
 | ||
|  | <<webflux-oauth2resourceserver-jwt-jwkseturi,How to Configure without Tying Resource Server startup to an authorization server's availability>> | ||
|  | 
 | ||
|  | <<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 any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, then the `jwk-set-uri` can be supplied as well: | ||
|  | 
 | ||
|  | [source,yaml] | ||
|  | ---- | ||
|  | spring: | ||
|  |   security: | ||
|  |     oauth2: | ||
|  |       resourceserver: | ||
|  |         jwt: | ||
|  |           issuer-uri: https://idp.example.com | ||
|  |           jwk-set-uri: https://idp.example.com/.well-known/jwks.json | ||
|  | ---- | ||
|  | 
 | ||
|  | [NOTE] | ||
|  | The JWK Set uri is not standardized, but can typically be found in the authorization server's documentation | ||
|  | 
 | ||
|  | Consequently, Resource Server will not ping the authorization server at startup. | ||
|  | We still specify the `issuer-uri` so that Resource Server still validates the `iss` claim on incoming JWTs. | ||
|  | 
 | ||
|  | [NOTE] | ||
|  | This property can also be supplied directly on the <<webflux-oauth2resourceserver-jwt-jwkseturi-dsl,DSL>>. | ||
|  | 
 | ||
|  | [[webflux-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 `SecurityWebFilterChain` that configures the app as a resource server. When including `spring-security-oauth2-jose`, this `SecurityWebFilterChain` looks like: | ||
|  | 
 | ||
|  | .Resource Server SecurityWebFilterChain | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { | ||
|  | 	http | ||
|  | 		.authorizeExchange(exchanges -> exchanges | ||
|  | 			.anyExchange().authenticated() | ||
|  | 		) | ||
|  | 		.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt) | ||
|  | 	return http.build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { | ||
|  |     return http { | ||
|  |         authorizeExchange { | ||
|  |             authorize(anyExchange, authenticated) | ||
|  |         } | ||
|  |         oauth2ResourceServer { | ||
|  |             jwt { } | ||
|  |         } | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | 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: | ||
|  | 
 | ||
|  | .Replacing SecurityWebFilterChain | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { | ||
|  | 	http | ||
|  | 		.authorizeExchange(exchanges -> exchanges | ||
|  | 			.pathMatchers("/message/**").hasAuthority("SCOPE_message:read") | ||
|  | 			.anyExchange().authenticated() | ||
|  | 		) | ||
|  | 		.oauth2ResourceServer(oauth2 -> oauth2 | ||
|  | 			.jwt(withDefaults()) | ||
|  | 		); | ||
|  | 	return http.build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { | ||
|  |     return http { | ||
|  |         authorizeExchange { | ||
|  |             authorize("/message/**", hasAuthority("SCOPE_message:read")) | ||
|  |             authorize(anyExchange, authenticated) | ||
|  |         } | ||
|  |         oauth2ResourceServer { | ||
|  |             jwt { } | ||
|  |         } | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | The above requires the scope of `message:read` for any URL that starts with `/messages/`. | ||
|  | 
 | ||
|  | Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration. | ||
|  | 
 | ||
|  | For example, the second `@Bean` Spring Boot creates is a `ReactiveJwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`: | ||
|  | 
 | ||
|  | .ReactiveJwtDecoder | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | public ReactiveJwtDecoder jwtDecoder() { | ||
|  |     return ReactiveJwtDecoders.fromIssuerLocation(issuerUri); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     return ReactiveJwtDecoders.fromIssuerLocation(issuerUri) | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | [NOTE] | ||
|  | Calling `{security-api-url}org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-[ReactiveJwtDecoders#fromIssuerLocation]` is what invokes the Provider Configuration or Authorization Server Metadata endpoint in order to derive the JWK Set Uri. | ||
|  | If the application doesn't expose a `ReactiveJwtDecoder` bean, then Spring Boot will expose the above default one. | ||
|  | 
 | ||
|  | And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`. | ||
|  | 
 | ||
|  | [[webflux-oauth2resourceserver-jwt-jwkseturi-dsl]] | ||
|  | ==== Using `jwkSetUri()` | ||
|  | 
 | ||
|  | 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: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { | ||
|  | 	http | ||
|  | 		.authorizeExchange(exchanges -> exchanges | ||
|  | 			.anyExchange().authenticated() | ||
|  | 		) | ||
|  | 		.oauth2ResourceServer(oauth2 -> oauth2 | ||
|  | 			.jwt(jwt -> jwt | ||
|  | 				.jwkSetUri("https://idp.example.com/.well-known/jwks.json") | ||
|  | 			) | ||
|  | 		); | ||
|  | 	return http.build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { | ||
|  |     return http { | ||
|  |         authorizeExchange { | ||
|  |             authorize(anyExchange, authenticated) | ||
|  |         } | ||
|  |         oauth2ResourceServer { | ||
|  |             jwt { | ||
|  |                 jwkSetUri = "https://idp.example.com/.well-known/jwks.json" | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | Using `jwkSetUri()` takes precedence over any configuration property. | ||
|  | 
 | ||
|  | [[webflux-oauth2resourceserver-jwt-decoder-dsl]] | ||
|  | ==== Using `decoder()` | ||
|  | 
 | ||
|  | More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { | ||
|  | 	http | ||
|  | 		.authorizeExchange(exchanges -> exchanges | ||
|  | 			.anyExchange().authenticated() | ||
|  | 		) | ||
|  | 		.oauth2ResourceServer(oauth2 -> oauth2 | ||
|  | 			.jwt(jwt -> jwt | ||
|  | 				.decoder(myCustomDecoder()) | ||
|  | 			) | ||
|  | 		); | ||
|  |     return http.build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { | ||
|  |     return http { | ||
|  |         authorizeExchange { | ||
|  |             authorize(anyExchange, authenticated) | ||
|  |         } | ||
|  |         oauth2ResourceServer { | ||
|  |             jwt { | ||
|  |                 jwtDecoder = myCustomDecoder() | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | This is handy when deeper configuration, like <<webflux-oauth2resourceserver-jwt-validation,validation>>, is necessary. | ||
|  | 
 | ||
|  | [[webflux-oauth2resourceserver-decoder-bean]] | ||
|  | ==== Exposing a `ReactiveJwtDecoder` `@Bean` | ||
|  | 
 | ||
|  | Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | public ReactiveJwtDecoder jwtDecoder() { | ||
|  |     return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     return ReactiveJwtDecoders.fromIssuerLocation(issuerUri) | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | [[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`: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | ReactiveJwtDecoder jwtDecoder() { | ||
|  |     return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) | ||
|  |             .jwsAlgorithm(RS512).build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) | ||
|  |             .jwsAlgorithm(RS512).build() | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | Calling `jwsAlgorithm` more than once will configure `NimbusReactiveJwtDecoder` to trust more than one algorithm, like so: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | ReactiveJwtDecoder jwtDecoder() { | ||
|  |     return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) | ||
|  |             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) | ||
|  |             .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build() | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | Or, you can call `jwsAlgorithms`: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | ReactiveJwtDecoder jwtDecoder() { | ||
|  |     return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) | ||
|  |             .jwsAlgorithms(algorithms -> { | ||
|  |                     algorithms.add(RS512); | ||
|  |                     algorithms.add(ES512); | ||
|  |             }).build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) | ||
|  |             .jwsAlgorithms { | ||
|  |                 it.add(RS512) | ||
|  |                 it.add(ES512) | ||
|  |             } | ||
|  |             .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`: | ||
|  | 
 | ||
|  | .BeanFactoryPostProcessor | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | BeanFactoryPostProcessor conversionServiceCustomizer() { | ||
|  |     return beanFactory -> | ||
|  |         beanFactory.getBean(RsaKeyConversionServicePostProcessor.class) | ||
|  |                 .setResourceLoader(new CustomResourceLoader()); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun conversionServiceCustomizer(): BeanFactoryPostProcessor { | ||
|  |     return BeanFactoryPostProcessor { beanFactory: ConfigurableListableBeanFactory -> | ||
|  |         beanFactory.getBean<RsaKeyConversionServicePostProcessor>() | ||
|  |                 .setResourceLoader(CustomResourceLoader()) | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | Specify your key's location: | ||
|  | 
 | ||
|  | [source,yaml] | ||
|  | ---- | ||
|  | key.location: hfds://my-key.pub | ||
|  | ---- | ||
|  | 
 | ||
|  | And then autowire the value: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Value("${key.location}") | ||
|  | RSAPublicKey key; | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Value("\${key.location}") | ||
|  | val key: RSAPublicKey? = null | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | [[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 | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | public ReactiveJwtDecoder jwtDecoder() { | ||
|  |     return NimbusReactiveJwtDecoder.withPublicKey(this.key).build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     return NimbusReactiveJwtDecoder.withPublicKey(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: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | public ReactiveJwtDecoder jwtDecoder() { | ||
|  |     return NimbusReactiveJwtDecoder.withSecretKey(this.key).build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     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: | ||
|  | 
 | ||
|  | `{ ..., "scope" : "messages contacts"}` | ||
|  | 
 | ||
|  | When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_". | ||
|  | 
 | ||
|  | This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { | ||
|  | 	http | ||
|  | 		.authorizeExchange(exchanges -> exchanges | ||
|  | 			.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") | ||
|  | 			.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") | ||
|  | 			.anyExchange().authenticated() | ||
|  | 		) | ||
|  | 		.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt); | ||
|  |     return http.build(); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { | ||
|  |     return http { | ||
|  |         authorizeExchange { | ||
|  |             authorize("/contacts/**", hasAuthority("SCOPE_contacts")) | ||
|  |             authorize("/messages/**", hasAuthority("SCOPE_messages")) | ||
|  |             authorize(anyExchange, authenticated) | ||
|  |         } | ||
|  |         oauth2ResourceServer { | ||
|  |             jwt { } | ||
|  |         } | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | Or similarly with method security: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @PreAuthorize("hasAuthority('SCOPE_messages')") | ||
|  | public Flux<Message> getMessages(...) {} | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @PreAuthorize("hasAuthority('SCOPE_messages')") | ||
|  | fun getMessages(): Flux<Message> { } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | [[webflux-oauth2resourceserver-jwt-authorization-extraction]] | ||
|  | ==== Extracting Authorities Manually | ||
|  | 
 | ||
|  | However, there are a number of circumstances where this default is insufficient. | ||
|  | For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute. | ||
|  | Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities. | ||
|  | 
 | ||
|  | To this end, the DSL exposes `jwtAuthenticationConverter()`: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { | ||
|  | 	http | ||
|  | 		.authorizeExchange(exchanges -> exchanges | ||
|  | 			.anyExchange().authenticated() | ||
|  | 		) | ||
|  | 		.oauth2ResourceServer(oauth2 -> oauth2 | ||
|  | 			.jwt(jwt -> jwt | ||
|  | 				.jwtAuthenticationConverter(grantedAuthoritiesExtractor()) | ||
|  | 			) | ||
|  | 		); | ||
|  | 	return http.build(); | ||
|  | } | ||
|  | 
 | ||
|  | Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() { | ||
|  |     JwtAuthenticationConverter jwtAuthenticationConverter = | ||
|  |             new JwtAuthenticationConverter(); | ||
|  |     jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter | ||
|  |             (new GrantedAuthoritiesExtractor()); | ||
|  |     return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { | ||
|  |     return http { | ||
|  |         authorizeExchange { | ||
|  |             authorize(anyExchange, authenticated) | ||
|  |         } | ||
|  |         oauth2ResourceServer { | ||
|  |             jwt { | ||
|  |                 jwtAuthenticationConverter = grantedAuthoritiesExtractor() | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | fun grantedAuthoritiesExtractor(): Converter<Jwt, Mono<AbstractAuthenticationToken>> { | ||
|  |     val jwtAuthenticationConverter = JwtAuthenticationConverter() | ||
|  |     jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor()) | ||
|  |     return 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. | ||
|  | 
 | ||
|  | That final converter might be something like `GrantedAuthoritiesExtractor` below: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | static class GrantedAuthoritiesExtractor | ||
|  |         implements Converter<Jwt, Collection<GrantedAuthority>> { | ||
|  | 
 | ||
|  |     public Collection<GrantedAuthority> convert(Jwt jwt) { | ||
|  |         Collection<?> authorities = (Collection<?>) | ||
|  |                 jwt.getClaims().getOrDefault("mycustomclaim", Collections.emptyList()); | ||
|  | 
 | ||
|  |         return authorities.stream() | ||
|  |                 .map(Object::toString) | ||
|  |                 .map(SimpleGrantedAuthority::new) | ||
|  |                 .collect(Collectors.toList()); | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | internal class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAuthority>> { | ||
|  |     override fun convert(jwt: Jwt): Collection<GrantedAuthority> { | ||
|  |         val authorities: List<Any> = jwt.claims | ||
|  |                 .getOrDefault("mycustomclaim", emptyList<Any>()) as List<Any> | ||
|  |         return authorities | ||
|  |                 .map { it.toString() } | ||
|  |                 .map { SimpleGrantedAuthority(it) } | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter<Jwt, Mono<AbstractAuthenticationToken>>`: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> { | ||
|  |     public AbstractAuthenticationToken convert(Jwt jwt) { | ||
|  |         return Mono.just(jwt).map(this::doConversion); | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> { | ||
|  |     override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> { | ||
|  |         return Mono.just(jwt).map(this::doConversion) | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | [[webflux-oauth2resourceserver-jwt-validation]] | ||
|  | === Configuring Validation | ||
|  | 
 | ||
|  | 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-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. | ||
|  | 
 | ||
|  | However, every server can experience clock drift, which can cause tokens to appear expired to one server, but not to another. | ||
|  | This can cause some implementation heartburn as the number of collaborating servers increases in a distributed system. | ||
|  | 
 | ||
|  | Resource Server uses `JwtTimestampValidator` to verify a token's validity window, and it can be configured with a `clockSkew` to alleviate the above problem: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | ReactiveJwtDecoder jwtDecoder() { | ||
|  |      NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) | ||
|  |              ReactiveJwtDecoders.fromIssuerLocation(issuerUri); | ||
|  | 
 | ||
|  |      OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>( | ||
|  |             new JwtTimestampValidator(Duration.ofSeconds(60)), | ||
|  |             new IssuerValidator(issuerUri)); | ||
|  | 
 | ||
|  |      jwtDecoder.setJwtValidator(withClockSkew); | ||
|  | 
 | ||
|  |      return jwtDecoder; | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder | ||
|  |     val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator( | ||
|  |             JwtTimestampValidator(Duration.ofSeconds(60)), | ||
|  |             JwtIssuerValidator(issuerUri)) | ||
|  |     jwtDecoder.setJwtValidator(withClockSkew) | ||
|  |     return jwtDecoder | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | [NOTE] | ||
|  | By default, Resource Server configures a clock skew of 60 seconds. | ||
|  | 
 | ||
|  | [[webflux-oauth2resourceserver-validation-custom]] | ||
|  | ==== Configuring a Custom Validator | ||
|  | 
 | ||
|  | Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | public class AudienceValidator implements OAuth2TokenValidator<Jwt> { | ||
|  |     OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null); | ||
|  | 
 | ||
|  |     public OAuth2TokenValidatorResult validate(Jwt jwt) { | ||
|  |         if (jwt.getAudience().contains("messaging")) { | ||
|  |             return OAuth2TokenValidatorResult.success(); | ||
|  |         } else { | ||
|  |             return OAuth2TokenValidatorResult.failure(error); | ||
|  |         } | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | class AudienceValidator : OAuth2TokenValidator<Jwt> { | ||
|  |     var error: OAuth2Error = OAuth2Error("invalid_token", "The required audience is missing", null) | ||
|  |     override fun validate(jwt: Jwt): OAuth2TokenValidatorResult { | ||
|  |         return if (jwt.audience.contains("messaging")) { | ||
|  |             OAuth2TokenValidatorResult.success() | ||
|  |         } else { | ||
|  |             OAuth2TokenValidatorResult.failure(error) | ||
|  |         } | ||
|  |     } | ||
|  | } | ||
|  | ---- | ||
|  | ==== | ||
|  | 
 | ||
|  | Then, to add into a resource server, it's a matter of specifying the `ReactiveJwtDecoder` instance: | ||
|  | 
 | ||
|  | ==== | ||
|  | .Java | ||
|  | [source,java,role="primary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | ReactiveJwtDecoder jwtDecoder() { | ||
|  |     NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) | ||
|  |             ReactiveJwtDecoders.fromIssuerLocation(issuerUri); | ||
|  | 
 | ||
|  |     OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(); | ||
|  |     OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); | ||
|  |     OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); | ||
|  | 
 | ||
|  |     jwtDecoder.setJwtValidator(withAudience); | ||
|  | 
 | ||
|  |     return jwtDecoder; | ||
|  | } | ||
|  | ---- | ||
|  | 
 | ||
|  | .Kotlin | ||
|  | [source,kotlin,role="secondary"] | ||
|  | ---- | ||
|  | @Bean | ||
|  | fun jwtDecoder(): ReactiveJwtDecoder { | ||
|  |     val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder | ||
|  |     val audienceValidator: OAuth2TokenValidator<Jwt> = AudienceValidator() | ||
|  |     val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri) | ||
|  |     val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator) | ||
|  |     jwtDecoder.setJwtValidator(withAudience) | ||
|  |     return jwtDecoder | ||
|  | } | ||
|  | ---- | ||
|  | ==== |