Steve Riesenberg 9db33f33c7
Revert unnecessary merges on 6.0.x
This commit removes unnecessary main-branch merges starting from
8750608b5bca45525c99d0a41a20ed02de93d8c7 and adds the following
needed commit(s) that were made afterward:

- 5dce82c48bc0b174838501c5a111b2de70822914
2023-10-31 15:11:45 -05:00

128 lines
4.6 KiB
Plaintext

= 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 can accept bearer tokens from two different authorization servers.
Alternately, your authorization server can represent a multiplicity of issuers.
In each case, two things need to be done and trade-offs are associated with how you choose to do them:
. Resolve the tenant.
. 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, you can do so with the `JwtIssuerReactiveAuthenticationManagerResolver`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver
.fromTrustedIssuers("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
.fromTrustedIssuers("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
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:
[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 to the issuer.
This approach lets us add and remove elements from the repository (shown as a `Map` in the preceding 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, such as an allowed list of issuers.
====