multitenancy.adoc 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. = OAuth 2.0 Resource Server Multi-tenancy
  2. [[webflux-oauth2resourceserver-multitenancy]]
  3. == Multi-tenancy
  4. A resource server is considered multi-tenant when there are multiple strategies for verifying a bearer token, keyed by some tenant identifier.
  5. For example, your resource server may accept bearer tokens from two different authorization servers.
  6. Or, your authorization server may represent a multiplicity of issuers.
  7. In each case, there are two things that need to be done and trade-offs associated with how you choose to do them:
  8. 1. Resolve the tenant
  9. 2. Propagate the tenant
  10. === Resolving the Tenant By Claim
  11. 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:
  12. [tabs]
  13. ======
  14. Java::
  15. +
  16. [source,java,role="primary"]
  17. ----
  18. JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver
  19. ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
  20. http
  21. .authorizeExchange(exchanges -> exchanges
  22. .anyExchange().authenticated()
  23. )
  24. .oauth2ResourceServer(oauth2 -> oauth2
  25. .authenticationManagerResolver(authenticationManagerResolver)
  26. );
  27. ----
  28. Kotlin::
  29. +
  30. [source,kotlin,role="secondary"]
  31. ----
  32. val customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo")
  33. return http {
  34. authorizeExchange {
  35. authorize(anyExchange, authenticated)
  36. }
  37. oauth2ResourceServer {
  38. authenticationManagerResolver = customAuthenticationManagerResolver
  39. }
  40. }
  41. ----
  42. ======
  43. This is nice because the issuer endpoints are loaded lazily.
  44. In fact, the corresponding `JwtReactiveAuthenticationManager` is instantiated only when the first request with the corresponding issuer is sent.
  45. This allows for an application startup that is independent from those authorization servers being up and available.
  46. ==== Dynamic Tenants
  47. Of course, you may not want to restart the application each time a new tenant is added.
  48. In this case, you can configure the `JwtIssuerReactiveAuthenticationManagerResolver` with a repository of `ReactiveAuthenticationManager` instances, which you can edit at runtime, like so:
  49. [tabs]
  50. ======
  51. Java::
  52. +
  53. [source,java,role="primary"]
  54. ----
  55. private Mono<ReactiveAuthenticationManager> addManager(
  56. Map<String, ReactiveAuthenticationManager> authenticationManagers, String issuer) {
  57. return Mono.fromCallable(() -> ReactiveJwtDecoders.fromIssuerLocation(issuer))
  58. .subscribeOn(Schedulers.boundedElastic())
  59. .map(JwtReactiveAuthenticationManager::new)
  60. .doOnNext(authenticationManager -> authenticationManagers.put(issuer, authenticationManager));
  61. }
  62. // ...
  63. JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver =
  64. new JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get);
  65. http
  66. .authorizeExchange(exchanges -> exchanges
  67. .anyExchange().authenticated()
  68. )
  69. .oauth2ResourceServer(oauth2 -> oauth2
  70. .authenticationManagerResolver(authenticationManagerResolver)
  71. );
  72. ----
  73. Kotlin::
  74. +
  75. [source,kotlin,role="secondary"]
  76. ----
  77. private fun addManager(
  78. authenticationManagers: MutableMap<String, ReactiveAuthenticationManager>, issuer: String): Mono<JwtReactiveAuthenticationManager> {
  79. return Mono.fromCallable { ReactiveJwtDecoders.fromIssuerLocation(issuer) }
  80. .subscribeOn(Schedulers.boundedElastic())
  81. .map { jwtDecoder: ReactiveJwtDecoder -> JwtReactiveAuthenticationManager(jwtDecoder) }
  82. .doOnNext { authenticationManager: JwtReactiveAuthenticationManager -> authenticationManagers[issuer] = authenticationManager }
  83. }
  84. // ...
  85. var customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get)
  86. return http {
  87. authorizeExchange {
  88. authorize(anyExchange, authenticated)
  89. }
  90. oauth2ResourceServer {
  91. authenticationManagerResolver = customAuthenticationManagerResolver
  92. }
  93. }
  94. ----
  95. ======
  96. In this case, you construct `JwtIssuerReactiveAuthenticationManagerResolver` with a strategy for obtaining the `ReactiveAuthenticationManager` given the issuer.
  97. This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime.
  98. NOTE: It would be unsafe to simply take any issuer and construct an `ReactiveAuthenticationManager` from it.
  99. The issuer should be one that the code can verify from a trusted source like an allowed list of issuers.