For the latest stable version, please use Spring Security 6.4.2!

OAuth 2.0 Resource Server Multi-tenancy

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:

  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, you can do so with the JwtIssuerReactiveAuthenticationManagerResolver:

  • Java

  • Kotlin

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)
    );
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:

  • Java

  • Kotlin

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)
    );
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.

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.