| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 | 
							- = OAuth 2.0 Resource Server Multi-tenancy
 
- [[oauth2reourceserver-opaqueandjwt]]
 
- == Supporting both JWT and Opaque Token
 
- In some cases, you may have a need to access both kinds of tokens.
 
- For example, you may support more than one tenant where one tenant issues JWTs and the other issues opaque tokens.
 
- If this decision must be made at request-time, then you can use an `AuthenticationManagerResolver` to achieve it, like so:
 
- [tabs]
 
- ======
 
- Java::
 
- +
 
- [source,java,role="primary"]
 
- ----
 
- @Bean
 
- AuthenticationManagerResolver<HttpServletRequest> tokenAuthenticationManagerResolver
 
-         (JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) {
 
-     AuthenticationManager jwt = new ProviderManager(new JwtAuthenticationProvider(jwtDecoder));
 
-     AuthenticationManager opaqueToken = new ProviderManager(
 
-             new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector));
 
-     return (request) -> useJwt(request) ? jwt : opaqueToken;
 
- }
 
- ----
 
- Kotlin::
 
- +
 
- [source,kotlin,role="secondary"]
 
- ----
 
- @Bean
 
- fun tokenAuthenticationManagerResolver
 
-         (jwtDecoder: JwtDecoder, opaqueTokenIntrospector: OpaqueTokenIntrospector):
 
-         AuthenticationManagerResolver<HttpServletRequest> {
 
-     val jwt = ProviderManager(JwtAuthenticationProvider(jwtDecoder))
 
-     val opaqueToken = ProviderManager(OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector));
 
-     return AuthenticationManagerResolver { request ->
 
-         if (useJwt(request)) {
 
-             jwt
 
-         } else {
 
-             opaqueToken
 
-         }
 
-     }
 
- }
 
- ----
 
- ======
 
- NOTE: The implementation of `useJwt(HttpServletRequest)` will likely depend on custom request material like the path.
 
- And then specify this `AuthenticationManagerResolver` in the DSL:
 
- .Authentication Manager Resolver
 
- [tabs]
 
- ======
 
- Java::
 
- +
 
- [source,java,role="primary"]
 
- ----
 
- http
 
-     .authorizeHttpRequests(authorize -> authorize
 
-         .anyRequest().authenticated()
 
-     )
 
-     .oauth2ResourceServer(oauth2 -> oauth2
 
-         .authenticationManagerResolver(this.tokenAuthenticationManagerResolver)
 
-     );
 
- ----
 
- Kotlin::
 
- +
 
- [source,kotlin,role="secondary"]
 
- ----
 
- http {
 
-     authorizeRequests {
 
-         authorize(anyRequest, authenticated)
 
-     }
 
-     oauth2ResourceServer {
 
-         authenticationManagerResolver = tokenAuthenticationManagerResolver()
 
-     }
 
- }
 
- ----
 
- Xml::
 
- +
 
- [source,xml,role="secondary"]
 
- ----
 
- <http>
 
-     <oauth2-resource-server authentication-manager-resolver-ref="tokenAuthenticationManagerResolver"/>
 
- </http>
 
- ----
 
- ======
 
- [[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 `JwtIssuerAuthenticationManagerResolver`, like so:
 
- .Multi-tenancy Tenant by JWT Claim
 
- [tabs]
 
- ======
 
- Java::
 
- +
 
- [source,java,role="primary"]
 
- ----
 
- JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
 
-     .fromTrustedIssuers("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
 
- http
 
-     .authorizeHttpRequests(authorize -> authorize
 
-         .anyRequest().authenticated()
 
-     )
 
-     .oauth2ResourceServer(oauth2 -> oauth2
 
-         .authenticationManagerResolver(authenticationManagerResolver)
 
-     );
 
- ----
 
- Kotlin::
 
- +
 
- [source,kotlin,role="secondary"]
 
- ----
 
- val customAuthenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
 
-     .fromTrustedIssuers("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo")
 
- http {
 
-     authorizeRequests {
 
-         authorize(anyRequest, authenticated)
 
-     }
 
-     oauth2ResourceServer {
 
-         authenticationManagerResolver = customAuthenticationManagerResolver
 
-     }
 
- }
 
- ----
 
- Xml::
 
- +
 
- [source,xml,role="secondary"]
 
- ----
 
- <http>
 
-     <oauth2-resource-server authentication-manager-resolver-ref="authenticationManagerResolver"/>
 
- </http>
 
- <bean id="authenticationManagerResolver"
 
-         class="org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver">
 
-     <constructor-arg>
 
-         <list>
 
-             <value>https://idp.example.org/issuerOne</value>
 
-             <value>https://idp.example.org/issuerTwo</value>
 
-         </list>
 
-     </constructor-arg>
 
- </bean>
 
- ----
 
- ======
 
- This is nice because the issuer endpoints are loaded lazily.
 
- In fact, the corresponding `JwtAuthenticationProvider` 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 `JwtIssuerAuthenticationManagerResolver` with a repository of `AuthenticationManager` instances, which you can edit at runtime, like so:
 
- [tabs]
 
- ======
 
- Java::
 
- +
 
- [source,java,role="primary"]
 
- ----
 
- private void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
 
- 	JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider
 
- 	        (JwtDecoders.fromIssuerLocation(issuer));
 
- 	authenticationManagers.put(issuer, authenticationProvider::authenticate);
 
- }
 
- // ...
 
- JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
 
-         new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
 
- http
 
-     .authorizeHttpRequests(authorize -> authorize
 
-         .anyRequest().authenticated()
 
-     )
 
-     .oauth2ResourceServer(oauth2 -> oauth2
 
-         .authenticationManagerResolver(authenticationManagerResolver)
 
-     );
 
- ----
 
- Kotlin::
 
- +
 
- [source,kotlin,role="secondary"]
 
- ----
 
- private fun addManager(authenticationManagers: MutableMap<String, AuthenticationManager>, issuer: String) {
 
-     val authenticationProvider = JwtAuthenticationProvider(JwtDecoders.fromIssuerLocation(issuer))
 
-     authenticationManagers[issuer] = AuthenticationManager {
 
-         authentication: Authentication? -> authenticationProvider.authenticate(authentication)
 
-     }
 
- }
 
- // ...
 
- val customAuthenticationManagerResolver: JwtIssuerAuthenticationManagerResolver =
 
-     JwtIssuerAuthenticationManagerResolver(authenticationManagers::get)
 
- http {
 
-     authorizeRequests {
 
-         authorize(anyRequest, authenticated)
 
-     }
 
-     oauth2ResourceServer {
 
-         authenticationManagerResolver = customAuthenticationManagerResolver
 
-     }
 
- }
 
- ----
 
- ======
 
- In this case, you construct `JwtIssuerAuthenticationManagerResolver` with a strategy for obtaining the `AuthenticationManager` 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 `AuthenticationManager` from it.
 
- The issuer should be one that the code can verify from a trusted source like a list of allowed issuers.
 
- ==== Parsing the Claim Only Once
 
- You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-architecture-jwtdecoder[`JwtDecoder`] later on in the request.
 
- This extra parsing can be alleviated by configuring the xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-architecture-jwtdecoder[`JwtDecoder`] directly with a `JWTClaimsSetAwareJWSKeySelector` from Nimbus:
 
- [tabs]
 
- ======
 
- Java::
 
- +
 
- [source,java,role="primary"]
 
- ----
 
- @Component
 
- public class TenantJWSKeySelector
 
-     implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
 
- 	private final TenantRepository tenants; <1>
 
- 	private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); <2>
 
- 	public TenantJWSKeySelector(TenantRepository tenants) {
 
- 		this.tenants = tenants;
 
- 	}
 
- 	@Override
 
- 	public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext)
 
- 			throws KeySourceException {
 
- 		return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant)
 
- 				.selectJWSKeys(jwsHeader, securityContext);
 
- 	}
 
- 	private String toTenant(JWTClaimsSet claimSet) {
 
- 		return (String) claimSet.getClaim("iss");
 
- 	}
 
- 	private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
 
- 		return Optional.ofNullable(this.tenants.findById(tenant)) <3>
 
- 		        .map(t -> t.getAttrbute("jwks_uri"))
 
- 				.map(this::fromUri)
 
- 				.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
 
- 	}
 
- 	private JWSKeySelector<SecurityContext> fromUri(String uri) {
 
- 		try {
 
- 			return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); <4>
 
- 		} catch (Exception ex) {
 
- 			throw new IllegalArgumentException(ex);
 
- 		}
 
- 	}
 
- }
 
- ----
 
- Kotlin::
 
- +
 
- [source,kotlin,role="secondary"]
 
- ----
 
- @Component
 
- class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
 
-     private val tenants: TenantRepository <1>
 
-     private val selectors: MutableMap<String, JWSKeySelector<SecurityContext>> = ConcurrentHashMap() <2>
 
-     init {
 
-         this.tenants = tenants
 
-     }
 
-     fun selectKeys(jwsHeader: JWSHeader?, jwtClaimsSet: JWTClaimsSet, securityContext: SecurityContext): List<Key?> {
 
-         return selectors.computeIfAbsent(toTenant(jwtClaimsSet)) { tenant: String -> fromTenant(tenant) }
 
-                 .selectJWSKeys(jwsHeader, securityContext)
 
-     }
 
-     private fun toTenant(claimSet: JWTClaimsSet): String {
 
-         return claimSet.getClaim("iss") as String
 
-     }
 
-     private fun fromTenant(tenant: String): JWSKeySelector<SecurityContext> {
 
-         return Optional.ofNullable(this.tenants.findById(tenant)) <3>
 
-                 .map { t -> t.getAttrbute("jwks_uri") }
 
-                 .map { uri: String -> fromUri(uri) }
 
-                 .orElseThrow { IllegalArgumentException("unknown tenant") }
 
-     }
 
-     private fun fromUri(uri: String): JWSKeySelector<SecurityContext?> {
 
-         return try {
 
-             JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(URL(uri)) <4>
 
-         } catch (ex: Exception) {
 
-             throw IllegalArgumentException(ex)
 
-         }
 
-     }
 
- }
 
- ----
 
- ======
 
- <1> A hypothetical source for tenant information
 
- <2> A cache for `JWKKeySelector`s, keyed by tenant identifier
 
- <3> Looking up the tenant is more secure than simply calculating the JWK Set endpoint on the fly - the lookup acts as a list of allowed tenants
 
- <4> Create a `JWSKeySelector` via the types of keys that come back from the JWK Set endpoint - the lazy lookup here means that you don't need to configure all tenants at startup
 
- The above key selector is a composition of many key selectors.
 
- It chooses which key selector to use based on the `iss` claim in the JWT.
 
- NOTE: To use this approach, make sure that the authorization server is configured to include the claim set as part of the token's signature.
 
- Without this, you have no guarantee that the issuer hasn't been altered by a bad actor.
 
- Next, we can construct a `JWTProcessor`:
 
- [tabs]
 
- ======
 
- Java::
 
- +
 
- [source,java,role="primary"]
 
- ----
 
- @Bean
 
- JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
 
- 	ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
 
-             new DefaultJWTProcessor();
 
- 	jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
 
- 	return jwtProcessor;
 
- }
 
- ----
 
- Kotlin::
 
- +
 
- [source,kotlin,role="secondary"]
 
- ----
 
- @Bean
 
- fun jwtProcessor(keySelector: JWTClaimsSetAwareJWSKeySelector<SecurityContext>): JWTProcessor<SecurityContext> {
 
-     val jwtProcessor = DefaultJWTProcessor<SecurityContext>()
 
-     jwtProcessor.jwtClaimsSetAwareJWSKeySelector = keySelector
 
-     return jwtProcessor
 
- }
 
- ----
 
- ======
 
- As you are already seeing, the trade-off for moving tenant-awareness down to this level is more configuration.
 
- We have just a bit more.
 
- Next, we still want to make sure you are validating the issuer.
 
- But, since the issuer may be different per JWT, then you'll need a tenant-aware validator, too:
 
- [tabs]
 
- ======
 
- Java::
 
- +
 
- [source,java,role="primary"]
 
- ----
 
- @Component
 
- public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
 
- 	private final TenantRepository tenants;
 
- 	private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
 
- 	public TenantJwtIssuerValidator(TenantRepository tenants) {
 
- 		this.tenants = tenants;
 
- 	}
 
- 	@Override
 
- 	public OAuth2TokenValidatorResult validate(Jwt token) {
 
- 		return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
 
- 				.validate(token);
 
- 	}
 
- 	private String toTenant(Jwt jwt) {
 
- 		return jwt.getIssuer();
 
- 	}
 
- 	private JwtIssuerValidator fromTenant(String tenant) {
 
- 		return Optional.ofNullable(this.tenants.findById(tenant))
 
- 		        .map(t -> t.getAttribute("issuer"))
 
- 				.map(JwtIssuerValidator::new)
 
- 				.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
 
- 	}
 
- }
 
- ----
 
- Kotlin::
 
- +
 
- [source,kotlin,role="secondary"]
 
- ----
 
- @Component
 
- class TenantJwtIssuerValidator(tenants: TenantRepository) : OAuth2TokenValidator<Jwt> {
 
-     private val tenants: TenantRepository
 
-     private val validators: MutableMap<String, JwtIssuerValidator> = ConcurrentHashMap()
 
-     override fun validate(token: Jwt): OAuth2TokenValidatorResult {
 
-         return validators.computeIfAbsent(toTenant(token)) { tenant: String -> fromTenant(tenant) }
 
-                 .validate(token)
 
-     }
 
-     private fun toTenant(jwt: Jwt): String {
 
-         return jwt.issuer.toString()
 
-     }
 
-     private fun fromTenant(tenant: String): JwtIssuerValidator {
 
-         return Optional.ofNullable(tenants.findById(tenant))
 
-                 .map({ t -> t.getAttribute("issuer") })
 
-                 .map({ JwtIssuerValidator() })
 
-                 .orElseThrow({ IllegalArgumentException("unknown tenant") })
 
-     }
 
-     init {
 
-         this.tenants = tenants
 
-     }
 
- }
 
- ----
 
- ======
 
- Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-architecture-jwtdecoder[`JwtDecoder`]:
 
- [tabs]
 
- ======
 
- Java::
 
- +
 
- [source,java,role="primary"]
 
- ----
 
- @Bean
 
- JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
 
- 	NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
 
- 	OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
 
- 			(JwtValidators.createDefault(), jwtValidator);
 
- 	decoder.setJwtValidator(validator);
 
- 	return decoder;
 
- }
 
- ----
 
- Kotlin::
 
- +
 
- [source,kotlin,role="secondary"]
 
- ----
 
- @Bean
 
- fun jwtDecoder(jwtProcessor: JWTProcessor<SecurityContext>?, jwtValidator: OAuth2TokenValidator<Jwt>?): JwtDecoder {
 
-     val decoder = NimbusJwtDecoder(jwtProcessor)
 
-     val validator: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(JwtValidators.createDefault(), jwtValidator)
 
-     decoder.setJwtValidator(validator)
 
-     return decoder
 
- }
 
- ----
 
- ======
 
- We've finished talking about resolving the tenant.
 
- If you've chosen to resolve the tenant by something other than a JWT claim, then you'll need to make sure you address your downstream resource servers in the same way.
 
- For example, if you are resolving it by subdomain, you may need to address the downstream resource server using the same subdomain.
 
- However, if you resolve it by a claim in the bearer token, read on to learn about xref:servlet/oauth2/resource-server/bearer-tokens.adoc#oauth2resourceserver-bearertoken-resolver[Spring Security's support for bearer token propagation].
 
 
  |