123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- = OAuth 2.0 Resource Server Multi-tenancy
- [[webflux-oauth2resourceserver-multitenancy]]
- == Multi-tenancy
- A resource server is considered multi-tenant when there are multiple strategies for verifying a bearer token, keyed by some tenant identifier.
- For example, your resource server may accept bearer tokens from two different authorization servers.
- Or, your authorization server may represent a multiplicity of issuers.
- In each case, there are two things that need to be done and trade-offs associated with how you choose to do them:
- 1. Resolve the tenant
- 2. Propagate the tenant
- === Resolving the Tenant By Claim
- 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:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver
- ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
- http
- .authorizeExchange(exchanges -> exchanges
- .anyExchange().authenticated()
- )
- .oauth2ResourceServer(oauth2 -> oauth2
- .authenticationManagerResolver(authenticationManagerResolver)
- );
- ----
- 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.
- ==== Dynamic Tenants
- 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:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- private Mono<ReactiveAuthenticationManager> addManager(
- Map<String, ReactiveAuthenticationManager> authenticationManagers, String issuer) {
- return Mono.fromCallable(() -> ReactiveJwtDecoders.fromIssuerLocation(issuer))
- .subscribeOn(Schedulers.boundedElastic())
- .map(JwtReactiveAuthenticationManager::new)
- .doOnNext(authenticationManager -> authenticationManagers.put(issuer, authenticationManager));
- }
- // ...
- JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver =
- new JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get);
- http
- .authorizeExchange(exchanges -> exchanges
- .anyExchange().authenticated()
- )
- .oauth2ResourceServer(oauth2 -> oauth2
- .authenticationManagerResolver(authenticationManagerResolver)
- );
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- private fun addManager(
- authenticationManagers: MutableMap<String, ReactiveAuthenticationManager>, issuer: String): Mono<JwtReactiveAuthenticationManager> {
- 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.
- NOTE: It would be unsafe to simply take any issuer and construct an `ReactiveAuthenticationManager` from it.
- The issuer should be one that the code can verify from a trusted source like an allowed list of issuers.
|