diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc index 9b79b71d20..59eca2b899 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc @@ -125,7 +125,10 @@ 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: -[source,java] +.Resource Server SecurityWebFilterChain +==== +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -138,11 +141,31 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { } ---- +.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: -[source,java] +.Replacing SecurityWebFilterChain +==== +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -158,13 +181,34 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { } ---- +.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`: -[source,java] +.ReactiveJwtDecoder +==== +.Java +[source,java,role="primary"] ---- @Bean public ReactiveJwtDecoder jwtDecoder() { @@ -172,6 +216,16 @@ public ReactiveJwtDecoder jwtDecoder() { } ---- +.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. @@ -183,7 +237,9 @@ And its configuration can be overridden using `jwkSetUri()` or replaced using `d An authorization server's JWK Set Uri can be configured <> or it can be supplied in the DSL: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -200,6 +256,25 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { } ---- +.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]] @@ -207,7 +282,9 @@ Using `jwkSetUri()` takes precedence over any configuration property. More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -224,6 +301,25 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { } ---- +.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 <>, is necessary. [[webflux-oauth2resourceserver-decoder-bean]] @@ -231,7 +327,9 @@ This is handy when deeper configuration, like < + beanFactory.getBean() + .setResourceLoader(CustomResourceLoader()) + } +} +---- +==== + Specify your key's location: ```yaml @@ -343,22 +510,46 @@ key.location: hfds://my-key.pub And then autowire the value: -```java +==== +.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 +==== +.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 @@ -366,7 +557,9 @@ public ReactiveJwtDecoder jwtDecoder() { 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] +==== +.Java +[source,java,role="primary"] ---- @Bean public ReactiveJwtDecoder jwtDecoder() { @@ -374,6 +567,16 @@ public ReactiveJwtDecoder jwtDecoder() { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return NimbusReactiveJwtDecoder.withSecretKey(this.key).build() +} +---- +==== + [[webflux-oauth2resourceserver-jwt-authorization]] === Configuring Authorization @@ -385,7 +588,9 @@ 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: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -400,14 +605,43 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { } ---- +.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: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @PreAuthorize("hasAuthority('SCOPE_messages')") public Flux getMessages(...) {} ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +fun getMessages(): Flux { } +---- +==== + [[webflux-oauth2resourceserver-jwt-authorization-extraction]] ==== Extracting Authorities Manually @@ -417,7 +651,9 @@ Or, at other times, the resource server may need to adapt the attribute or a com To this end, the DSL exposes `jwtAuthenticationConverter()`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -442,12 +678,39 @@ Converter> grantedAuthoritiesExtractor() } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwtAuthenticationConverter = grantedAuthoritiesExtractor() + } + } + } +} + +fun grantedAuthoritiesExtractor(): Converter> { + 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: -[source,java] +==== +.Java +[source,java,role="primary"] ---- static class GrantedAuthoritiesExtractor implements Converter> { @@ -464,9 +727,26 @@ static class GrantedAuthoritiesExtractor } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +internal class GrantedAuthoritiesExtractor : Converter> { + override fun convert(jwt: Jwt): Collection { + val authorities: List = jwt.claims + .getOrDefault("mycustomclaim", emptyList()) as List + return authorities + .map { it.toString() } + .map { SimpleGrantedAuthority(it) } + } +} +---- +==== + For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter>`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- static class CustomAuthenticationConverter implements Converter> { public AbstractAuthenticationToken convert(Jwt jwt) { @@ -475,6 +755,17 @@ static class CustomAuthenticationConverter implements Converter> { + override fun convert(jwt: Jwt): Mono { + return Mono.just(jwt).map(this::doConversion) + } +} +---- +==== + [[webflux-oauth2resourceserver-jwt-validation]] === Configuring Validation @@ -492,7 +783,9 @@ 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: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean ReactiveJwtDecoder jwtDecoder() { @@ -509,6 +802,21 @@ ReactiveJwtDecoder jwtDecoder() { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder + val withClockSkew: OAuth2TokenValidator = DelegatingOAuth2TokenValidator( + JwtTimestampValidator(Duration.ofSeconds(60)), + JwtIssuerValidator(issuerUri)) + jwtDecoder.setJwtValidator(withClockSkew) + return jwtDecoder +} +---- +==== + [NOTE] By default, Resource Server configures a clock skew of 30 seconds. @@ -517,7 +825,9 @@ By default, Resource Server configures a clock skew of 30 seconds. Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API: -[source,java] +==== +.Java +[source,java,role="primary"] ---- public class AudienceValidator implements OAuth2TokenValidator { OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null); @@ -532,9 +842,27 @@ public class AudienceValidator implements OAuth2TokenValidator { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class AudienceValidator : OAuth2TokenValidator { + 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: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean ReactiveJwtDecoder jwtDecoder() { @@ -550,6 +878,22 @@ ReactiveJwtDecoder jwtDecoder() { return jwtDecoder; } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder + val audienceValidator: OAuth2TokenValidator = AudienceValidator() + val withIssuer: OAuth2TokenValidator = JwtValidators.createDefaultWithIssuer(issuerUri) + val withAudience: OAuth2TokenValidator = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator) + jwtDecoder.setJwtValidator(withAudience) + return jwtDecoder +} +---- +==== + [[webflux-oauth2resourceserver-opaque-minimaldependencies]] === Minimal Dependencies for Introspection As described in <> most of Resource Server support is collected in `spring-security-oauth2-resource-server`. @@ -630,7 +974,9 @@ Once a token is authenticated, an instance of `BearerTokenAuthentication` is set This means that it's available in `@Controller` methods when using `@EnableWebFlux` in your configuration: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/foo") public Mono foo(BearerTokenAuthentication authentication) { @@ -638,9 +984,21 @@ public Mono foo(BearerTokenAuthentication authentication) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/foo") +fun foo(authentication: BearerTokenAuthentication): Mono { + return Mono.just(authentication.tokenAttributes["sub"].toString() + " is the subject") +} +---- +==== + Since `BearerTokenAuthentication` holds an `OAuth2AuthenticatedPrincipal`, that also means that it's available to controller methods, too: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/foo") public Mono foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) { @@ -648,18 +1006,41 @@ public Mono foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal pr } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/foo") +fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): Mono { + return Mono.just(principal.getAttribute("sub").toString() + " 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 +==== +.Java +[source,java,role="primary"] +---- @PreAuthorize("principal?.attributes['sub'] == 'foo'") public Mono forFoosEyesOnly() { return Mono.just("foo"); } -``` +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@PreAuthorize("principal.attributes['sub'] == 'foo'") +fun forFoosEyesOnly(): Mono { + return Mono.just("foo") +} +---- +==== [[webflux-oauth2resourceserver-opaque-sansboot]] === Overriding or Replacing Boot Auto Configuration @@ -669,7 +1050,9 @@ 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] +==== +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -682,11 +1065,31 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { } + } + } +} +---- +==== + 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] +.Replacing SecurityWebFilterChain +==== +.Java +[source,java,role="primary"] ---- @EnableWebFluxSecurity public class MyCustomSecurityConfiguration { @@ -707,13 +1110,35 @@ public class MyCustomSecurityConfiguration { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize("/messages/**", hasAuthority("SCOPE_message:read")) + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspector = myIntrospector() + } + } + } +} +---- +==== + The above requires the scope of `message:read` for any URL that starts with `/messages/`. Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration. For example, the second `@Bean` Spring Boot creates is a `ReactiveOpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public ReactiveOpaqueTokenIntrospector introspector() { @@ -721,6 +1146,16 @@ public ReactiveOpaqueTokenIntrospector introspector() { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return 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()`. @@ -730,7 +1165,9 @@ And its configuration can be overridden using `introspectionUri()` and `introspe An authorization server's Introspection Uri can be configured <> or it can be supplied in the DSL: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @EnableWebFluxSecurity public class DirectlyConfiguredIntrospectionUri { @@ -751,6 +1188,26 @@ public class DirectlyConfiguredIntrospectionUri { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspectionUri = "https://idp.example.com/introspect" + introspectionClientCredentials("client", "secret") + } + } + } +} +---- +==== + Using `introspectionUri()` takes precedence over any configuration property. [[webflux-oauth2resourceserver-opaque-introspector-dsl]] @@ -758,7 +1215,9 @@ Using `introspectionUri()` takes precedence over any configuration property. More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of `ReactiveOpaqueTokenIntrospector`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @EnableWebFluxSecurity public class DirectlyConfiguredIntrospector { @@ -778,6 +1237,25 @@ public class DirectlyConfiguredIntrospector { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspector = myCustomIntrospector() + } + } + } +} +---- +==== + This is handy when deeper configuration, like <>or <> is necessary. [[webflux-oauth2resourceserver-opaque-introspector-bean]] @@ -785,7 +1263,9 @@ This is handy when deeper configuration, like < getMessages(...) {} -``` +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +fun getMessages(): Flux { } +---- +==== [[webflux-oauth2resourceserver-opaque-authorization-extraction]] ==== Extracting Authorities Manually @@ -847,7 +1370,9 @@ Then Resource Server would generate an `Authentication` with two authorities, on 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] +==== +.Java +[source,java,role="primary"] ---- public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector { private ReactiveOpaqueTokenIntrospector delegate = @@ -868,9 +1393,33 @@ public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueT } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class CustomAuthoritiesOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector { + private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + override fun introspect(token: String): Mono { + return delegate.introspect(token) + .map { principal: OAuth2AuthenticatedPrincipal -> + DefaultOAuth2AuthenticatedPrincipal( + principal.name, principal.attributes, extractAuthorities(principal)) + } + } + + private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection { + val scopes = principal.getAttribute>(OAuth2IntrospectionClaimNames.SCOPE) + return scopes + .map { SimpleGrantedAuthority(it) } + } +} +---- +==== + Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public ReactiveOpaqueTokenIntrospector introspector() { @@ -878,6 +1427,16 @@ public ReactiveOpaqueTokenIntrospector introspector() { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return CustomAuthoritiesOpaqueTokenIntrospector() +} +---- +==== + [[webflux-oauth2resourceserver-opaque-jwt-introspector]] === Using Introspection with JWTs @@ -908,7 +1467,9 @@ 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] +==== +.Java +[source,java,role="primary"] ---- public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector { private ReactiveOpaqueTokenIntrospector delegate = @@ -933,9 +1494,36 @@ public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospect } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class JwtOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector { + private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val jwtDecoder: ReactiveJwtDecoder = NimbusReactiveJwtDecoder(ParseOnlyJWTProcessor()) + override fun introspect(token: String): Mono { + return delegate.introspect(token) + .flatMap { jwtDecoder.decode(token) } + .map { jwt: Jwt -> DefaultOAuth2AuthenticatedPrincipal(jwt.claims, NO_AUTHORITIES) } + } + + private class ParseOnlyJWTProcessor : Converter> { + override fun convert(jwt: JWT): Mono { + return try { + Mono.just(jwt.jwtClaimsSet) + } catch (e: Exception) { + Mono.error(e) + } + } + } +} +---- +==== + Thereafter, this custom introspector can be configured simply by exposing it as a `@Bean`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public ReactiveOpaqueTokenIntrospector introspector() { @@ -943,6 +1531,16 @@ public ReactiveOpaqueTokenIntrospector introspector() { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return JwtOpaqueTokenIntrospector() +} +---- +==== + [[webflux-oauth2resourceserver-opaque-userinfo]] === Calling a `/userinfo` Endpoint @@ -957,7 +1555,9 @@ 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 -[source,java] +==== +.Java +[source,java,role="primary"] ---- public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector { private final ReactiveOpaqueTokenIntrospector delegate = @@ -985,10 +1585,37 @@ public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntro } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector { + private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val oauth2UserService: ReactiveOAuth2UserService = DefaultReactiveOAuth2UserService() + private val repository: ReactiveClientRegistrationRepository? = null + + // ... constructor + override fun introspect(token: String?): Mono { + return Mono.zip(delegate.introspect(token), repository!!.findByRegistrationId("registration-id")) + .map { t: Tuple2 -> + val authorized = t.t1 + val clientRegistration = t.t2 + val issuedAt: Instant? = authorized.getAttribute(ISSUED_AT) + val expiresAt: Instant? = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT) + val accessToken = OAuth2AccessToken(BEARER, token, issuedAt, expiresAt) + OAuth2UserRequest(clientRegistration, accessToken) + } + .flatMap { userRequest: OAuth2UserRequest -> oauth2UserService.loadUser(userRequest) } + } +} +---- +==== + 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] +==== +.Java +[source,java,role="primary"] ---- public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector { private final ReactiveOpaqueTokenIntrospector delegate = @@ -1003,15 +1630,42 @@ public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntro } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector { + private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val rest: WebClient = WebClient.create() + + override fun introspect(token: String): Mono { + return 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] +==== +.Java +[source,java,role="primary"] ---- @Bean ReactiveOpaqueTokenIntrospector introspector() { - return new UserInfoOpaqueTokenIntrospector(...); + return new UserInfoOpaqueTokenIntrospector(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return UserInfoOpaqueTokenIntrospector() } ---- +==== [[webflux-oauth2resourceserver-multitenancy]] == Multi-tenancy @@ -1030,7 +1684,9 @@ In each case, there are two things that need to be done and trade-offs associate One way to differentiate tenants is by the issuer claim. Since the issuer claim accompanies signed JWTs, this can be done with the `JwtIssuerReactiveAuthenticationManagerResolver`, like so: -[source,java] +==== +.Java +[source,java,role="primary"] ---- JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); @@ -1044,6 +1700,22 @@ http ); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo") + +return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + authenticationManagerResolver = customAuthenticationManagerResolver + } +} +---- +==== + This is nice because the issuer endpoints are loaded lazily. In fact, the corresponding `JwtReactiveAuthenticationManager` is instantiated only when the first request with the corresponding issuer is sent. This allows for an application startup that is independent from those authorization servers being up and available. @@ -1053,7 +1725,9 @@ This allows for an application startup that is independent from those authorizat Of course, you may not want to restart the application each time a new tenant is added. In this case, you can configure the `JwtIssuerReactiveAuthenticationManagerResolver` with a repository of `ReactiveAuthenticationManager` instances, which you can edit at runtime, like so: -[source,java] +==== +.Java +[source,java,role="primary"] ---- private Mono addManager( Map authenticationManagers, String issuer) { @@ -1078,6 +1752,31 @@ http ); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +private fun addManager( + authenticationManagers: MutableMap, issuer: String): Mono { + return Mono.fromCallable { ReactiveJwtDecoders.fromIssuerLocation(issuer) } + .subscribeOn(Schedulers.boundedElastic()) + .map { jwtDecoder: ReactiveJwtDecoder -> JwtReactiveAuthenticationManager(jwtDecoder) } + .doOnNext { authenticationManager: JwtReactiveAuthenticationManager -> authenticationManagers[issuer] = authenticationManager } +} + +// ... + +var customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get) +return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + authenticationManagerResolver = customAuthenticationManagerResolver + } +} +---- +==== + In this case, you construct `JwtIssuerReactiveAuthenticationManagerResolver` with a strategy for obtaining the `ReactiveAuthenticationManager` given the issuer. This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime. @@ -1105,6 +1804,18 @@ http .bearerTokenConverter(converter) ); ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val converter = ServerBearerTokenAuthenticationConverter() +converter.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION) +return http { + oauth2ResourceServer { + bearerTokenConverter = converter + } +} +---- ==== == Bearer Token Propagation @@ -1112,7 +1823,9 @@ http Now that you're in possession of a bearer token, it might be handy to pass that to downstream services. This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.html[ServerBearerExchangeFilterFunction]`, which you can see in the following example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public WebClient rest() { @@ -1122,12 +1835,26 @@ public WebClient rest() { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun rest(): WebClient { + return WebClient.builder() + .filter(ServerBearerExchangeFilterFunction()) + .build() +} +---- +==== + When the above `WebClient` is used to perform requests, Spring Security will look up the current `Authentication` and extract any `{security-api-url}org/springframework/security/oauth2/core/AbstractOAuth2Token.html[AbstractOAuth2Token]` credential. Then, it will propagate that token in the `Authorization` header. For example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- this.rest.get() .uri("https://other-service.example.com/endpoint") @@ -1135,11 +1862,23 @@ this.rest.get() .bodyToMono(String.class) ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +this.rest.get() + .uri("https://other-service.example.com/endpoint") + .retrieve() + .bodyToMono() +---- +==== + Will invoke the `https://other-service.example.com/endpoint`, adding the bearer token `Authorization` header for you. In places where you need to override this behavior, it's a simple matter of supplying the header yourself, like so: -[source,java] +==== +.Java +[source,java,role="primary"] ---- this.rest.get() .uri("https://other-service.example.com/endpoint") @@ -1148,6 +1887,17 @@ this.rest.get() .bodyToMono(String.class) ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +rest.get() + .uri("https://other-service.example.com/endpoint") + .headers { it.setBearerAuth(overridingToken) } + .retrieve() + .bodyToMono() +---- +==== + In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain. [NOTE]