Skip to content

Latest commit

 

History

History
186 lines (135 loc) · 11 KB

how-to-multitenancy.adoc

File metadata and controls

186 lines (135 loc) · 11 KB

How-to: Implement Multitenancy

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.

Define the tenant identifier

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".

Enable multiple issuers

Support for using multiple issuers per host is disabled by default. To enable, add the following configuration:

AuthorizationServerSettingsConfig
link:{examples-dir}/main/java/sample/multitenancy/AuthorizationServerSettingsConfig.java[role=include]
  1. Set to true to allow usage of multiple issuers per host.

Create a component registry

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:

TenantPerIssuerComponentRegistry
link:{examples-dir}/main/java/sample/multitenancy/TenantPerIssuerComponentRegistry.java[role=include]
  1. 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.

Create multi-tenant components

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.

Multi-tenant RegisteredClientRepository

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:

RegisteredClientRepositoryConfig
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.
  1. A JdbcRegisteredClientRepository instance mapped to issuer identifier issuer1 and using a dedicated DataSource.

  2. A JdbcRegisteredClientRepository instance mapped to issuer identifier issuer2 and using a dedicated DataSource.

  3. A composite implementation of a RegisteredClientRepository that delegates to a JdbcRegisteredClientRepository mapped to the "requested" issuer identifier.

  4. Obtain the JdbcRegisteredClientRepository that is mapped to the "requested" issuer identifier indicated by AuthorizationServerContext.getIssuer().

  5. 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:

DataSourceConfig
link:{examples-dir}/main/java/sample/multitenancy/DataSourceConfig.java[role=include]
  1. Use a separate H2 database instance using issuer1-db as the name.

  2. Use a separate H2 database instance using issuer2-db as the name.

Multi-tenant OAuth2AuthorizationService

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:

OAuth2AuthorizationServiceConfig
link:{examples-dir}/main/java/sample/multitenancy/OAuth2AuthorizationServiceConfig.java[role=include]
  1. A JdbcOAuth2AuthorizationService instance mapped to issuer identifier issuer1 and using a dedicated DataSource.

  2. A JdbcOAuth2AuthorizationService instance mapped to issuer identifier issuer2 and using a dedicated DataSource.

  3. A composite implementation of an OAuth2AuthorizationService that delegates to a JdbcOAuth2AuthorizationService mapped to the "requested" issuer identifier.

  4. Obtain the JdbcOAuth2AuthorizationService that is mapped to the "requested" issuer identifier indicated by AuthorizationServerContext.getIssuer().

  5. 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:

OAuth2AuthorizationConsentServiceConfig
link:{examples-dir}/main/java/sample/multitenancy/OAuth2AuthorizationConsentServiceConfig.java[role=include]
  1. A JdbcOAuth2AuthorizationConsentService instance mapped to issuer identifier issuer1 and using a dedicated DataSource.

  2. A JdbcOAuth2AuthorizationConsentService instance mapped to issuer identifier issuer2 and using a dedicated DataSource.

  3. A composite implementation of an OAuth2AuthorizationConsentService that delegates to a JdbcOAuth2AuthorizationConsentService mapped to the "requested" issuer identifier.

  4. Obtain the JdbcOAuth2AuthorizationConsentService that is mapped to the "requested" issuer identifier indicated by AuthorizationServerContext.getIssuer().

  5. If unable to find JdbcOAuth2AuthorizationConsentService, then error since the "requested" issuer identifier is not in the allowlist of approved issuers.

Multi-tenant JWKSource

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:

JWKSourceConfig
link:{examples-dir}/main/java/sample/multitenancy/JWKSourceConfig.java[role=include]
  1. A JWKSet instance mapped to issuer identifier issuer1.

  2. A JWKSet instance mapped to issuer identifier issuer2.

  3. A composite implementation of an JWKSource<SecurityContext> that uses the JWKSet mapped to the "requested" issuer identifier.

  4. Obtain the JWKSet that is mapped to the "requested" issuer identifier indicated by AuthorizationServerContext.getIssuer().

  5. If unable to find JWKSet, then error since the "requested" issuer identifier is not in the allowlist of approved issuers.

Add Tenants Dynamically

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:

TenantService
link:{examples-dir}/main/java/sample/multitenancy/TenantService.java[role=include]