You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.6 KiB
127 lines
4.6 KiB
= 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) -> authenticationManager.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. |
|
====
|
|
|