This guide shows how to customize Spring Authorization Server to support multiple issuers per host in a multi-tenant hosting configuration. The purpose of this guide is to demonstrate a general pattern for building multi-tenant capable components for Spring Authorization Server, which can also be applied to other components to suit your needs.
The OpenID Connect 1.0 Provider Configuration Endpoint and OAuth2 Authorization Server Metadata Endpoint allow for path components in the issuer identifier value, which effectively enables supporting multiple issuers per host.
For example, an OpenID Provider Configuration Request "http://localhost:9000/issuer1/.well-known/openid-configuration" or an Authorization Server Metadata Request "http://localhost:9000/.well-known/oauth-authorization-server/issuer1" would return the following configuration metadata:
{
"issuer": "http://localhost:9000/issuer1",
"authorization_endpoint": "http://localhost:9000/issuer1/oauth2/authorize",
"token_endpoint": "http://localhost:9000/issuer1/oauth2/token",
"jwks_uri": "http://localhost:9000/issuer1/oauth2/jwks",
"revocation_endpoint": "http://localhost:9000/issuer1/oauth2/revoke",
"introspection_endpoint": "http://localhost:9000/issuer1/oauth2/introspect",
...
}
Note
|
The base URL of the Protocol Endpoints is the issuer identifier value. |
Essentially, an issuer identifier with a path component represents the "tenant identifier".
Support for using multiple issuers per host is disabled by default. To enable, add the following configuration:
link:{examples-dir}/main/java/sample/multitenancy/AuthorizationServerSettingsConfig.java[role=include]
-
Set to
true
to allow usage of multiple issuers per host.
We start by building a simple registry for managing the concrete components for each tenant. The registry contains the logic for retrieving a concrete implementation of a particular class using the issuer identifier value.
We will use the following class in each of the delegating implementations below:
link:{examples-dir}/main/java/sample/multitenancy/TenantPerIssuerComponentRegistry.java[role=include]
-
Component registration implicitly enables an allowlist of approved issuers that can be used.
Tip
|
This registry is designed to allow components to be easily registered at startup to support adding tenants statically, but also supports adding tenants dynamically at runtime. |
The components that require multi-tenant capability are:
For each of these components, an implementation of a composite can be provided that delegates to the concrete component associated to the "requested" issuer identifier.
Let’s step through a scenario of how to customize Spring Authorization Server to support 2x tenants for each multi-tenant capable component.
The following example shows a sample implementation of a RegisteredClientRepository
that is composed of 2x JdbcRegisteredClientRepository
instances, where each instance is mapped to an issuer identifier:
link:{examples-dir}/main/java/sample/multitenancy/RegisteredClientRepositoryConfig.java[role=include]
Tip
|
Click on the "Expand folded text" icon in the code sample above to display the full example. |
-
A
JdbcRegisteredClientRepository
instance mapped to issuer identifierissuer1
and using a dedicatedDataSource
. -
A
JdbcRegisteredClientRepository
instance mapped to issuer identifierissuer2
and using a dedicatedDataSource
. -
A composite implementation of a
RegisteredClientRepository
that delegates to aJdbcRegisteredClientRepository
mapped to the "requested" issuer identifier. -
Obtain the
JdbcRegisteredClientRepository
that is mapped to the "requested" issuer identifier indicated byAuthorizationServerContext.getIssuer()
. -
If unable to find
JdbcRegisteredClientRepository
, then error since the "requested" issuer identifier is not in the allowlist of approved issuers.
Important
|
Explicitly configuring the issuer identifier via AuthorizationServerSettings.builder().issuer("http://localhost:9000") forces to a single-tenant configuration. Avoid explicitly configuring the issuer identifier when using a multi-tenant hosting configuration.
|
In the preceding example, each of the JdbcRegisteredClientRepository
instances are configured with a JdbcTemplate
and associated DataSource
.
This is important in a multi-tenant configuration as a primary requirement is to have the ability to isolate the data from each tenant.
Configuring a dedicated DataSource
for each component instance provides the flexibility to isolate the data in its own schema within the same database instance or alternatively isolate the data in a separate database instance altogether.
The following example shows a sample configuration of 2x DataSource
@Bean
(one for each tenant) that are used by the multi-tenant capable components:
link:{examples-dir}/main/java/sample/multitenancy/DataSourceConfig.java[role=include]
-
Use a separate H2 database instance using
issuer1-db
as the name. -
Use a separate H2 database instance using
issuer2-db
as the name.
The following example shows a sample implementation of an OAuth2AuthorizationService
that is composed of 2x JdbcOAuth2AuthorizationService
instances, where each instance is mapped to an issuer identifier:
link:{examples-dir}/main/java/sample/multitenancy/OAuth2AuthorizationServiceConfig.java[role=include]
-
A
JdbcOAuth2AuthorizationService
instance mapped to issuer identifierissuer1
and using a dedicatedDataSource
. -
A
JdbcOAuth2AuthorizationService
instance mapped to issuer identifierissuer2
and using a dedicatedDataSource
. -
A composite implementation of an
OAuth2AuthorizationService
that delegates to aJdbcOAuth2AuthorizationService
mapped to the "requested" issuer identifier. -
Obtain the
JdbcOAuth2AuthorizationService
that is mapped to the "requested" issuer identifier indicated byAuthorizationServerContext.getIssuer()
. -
If unable to find
JdbcOAuth2AuthorizationService
, then error since the "requested" issuer identifier is not in the allowlist of approved issuers.
The following example shows a sample implementation of an OAuth2AuthorizationConsentService
that is composed of 2x JdbcOAuth2AuthorizationConsentService
instances, where each instance is mapped to an issuer identifier:
link:{examples-dir}/main/java/sample/multitenancy/OAuth2AuthorizationConsentServiceConfig.java[role=include]
-
A
JdbcOAuth2AuthorizationConsentService
instance mapped to issuer identifierissuer1
and using a dedicatedDataSource
. -
A
JdbcOAuth2AuthorizationConsentService
instance mapped to issuer identifierissuer2
and using a dedicatedDataSource
. -
A composite implementation of an
OAuth2AuthorizationConsentService
that delegates to aJdbcOAuth2AuthorizationConsentService
mapped to the "requested" issuer identifier. -
Obtain the
JdbcOAuth2AuthorizationConsentService
that is mapped to the "requested" issuer identifier indicated byAuthorizationServerContext.getIssuer()
. -
If unable to find
JdbcOAuth2AuthorizationConsentService
, then error since the "requested" issuer identifier is not in the allowlist of approved issuers.
And finally, the following example shows a sample implementation of a JWKSource<SecurityContext>
that is composed of 2x JWKSet
instances, where each instance is mapped to an issuer identifier:
link:{examples-dir}/main/java/sample/multitenancy/JWKSourceConfig.java[role=include]
-
A
JWKSet
instance mapped to issuer identifierissuer1
. -
A
JWKSet
instance mapped to issuer identifierissuer2
. -
A composite implementation of an
JWKSource<SecurityContext>
that uses theJWKSet
mapped to the "requested" issuer identifier. -
Obtain the
JWKSet
that is mapped to the "requested" issuer identifier indicated byAuthorizationServerContext.getIssuer()
. -
If unable to find
JWKSet
, then error since the "requested" issuer identifier is not in the allowlist of approved issuers.
If the number of tenants is dynamic and can change at runtime, defining each DataSource
as a @Bean
may not be feasible.
In this case, the DataSource
and corresponding components can be registered through other means at application startup and/or runtime.
The following example shows a Spring @Service
capable of adding tenants dynamically:
link:{examples-dir}/main/java/sample/multitenancy/TenantService.java[role=include]