eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

Course – LSS – NPI (cat=Spring Security)
announcement - icon

If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:

>> LEARN SPRING SECURITY

1. Introduction

The Spring Authorization Server started a few years ago as a side project under Spring’s large portfolio. Since then, it has gained traction and, as of version 7, has been incorporated into Spring Security as an official module.

In this tutorial, we’ll explore how to use it in multitenancy scenarios, a feature that can help serve multiple distinct customers on a single server deployment.

2. Quick Recap

We’ve already covered Spring Authorization Server’s – or SAS, for short – basic usage in previous tutorials, but let’s start with a quick recap.

In a nutshell, SAS is a Spring-based library that lets us quickly implement an OpenID Connect/OAuth 2.0-compliant identity provider. The implementation is built on top of the mature Spring Security framework, making it easy to either embed or extend using familiar Spring-related patterns.

With the project’s migration into Spring Security, there were some changes that made it even easier to integrate this library into users’ applications:

  • Version alignment with other Spring Security dependencies
  • HttpSecurity’s DSL integration
  • Single documentation site, improving developer experience

Moreover, Spring Boot 4 also includes new starter modules, which simplify SAS’s usage. We can now have a fully functional OpenID Connect server in a few minutes using start.spring.io, adding the required dependencies, and configuring a few properties.

Warning: as of this writing, SAS version 7.x is limited to servlet-based applications. If we have a reactive web-based SAS application, we can either rewrite it to use the MVC-based APIs or, at least for now, stay on the 1.x version until we’re ready to upgrade.

3. Multitenancy in Spring Authorization Server

Despite the quality-of-life improvements, one of SAS’s available features that does require some code is multitenancy support.

In a standard SAS implementation, we generally register one or more client applications using properties or database-backed stores, which share the same set of keys and, critically, the same issuer.

This means that SAS-generated tokens created for one client may also be considered valid by another client, since they’ll be signed by the same key. Depending on the client’s configuration, this may pose a security risk: since not all clients implement audience validation, a malicious or compromised client may acquire a token and use it to access data intended for other clients.

Using multitenancy, we can segregate clients so that each tenant uses its own key pairs to sign tokens. Even if clients from one tenant are compromised, the tokens won’t be recognized by other tenants.

SAS follows the OpenID Connect recommendation regarding multitenancy. As per the spec, the issuer URL may have a path after the host:port part. In SAS’s implementation, we use the path’s last part as the tenant identifier:

  • Issuer URL: https://example.com:9443/issuer1 => Tenant identifier: issuer1
  • Issuer URL: https://example.com:9443/issuer2 => Tenant identifier: issuer2

4. Project Setup

Let’s create a simple SAS-based project to see how multitenancy works in practice.

First, let’s add the required Spring Boot starter Maven dependencies to our project:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security-oauth2-authorization-server</artifactId>
    <version>4.0.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security-oauth2-authorization-server-test</artifactId>
    <version>4.0.1</version>
    <scope>test</scope>
</dependency>

The latest versions of these dependencies are available on Maven Central:

We also need to enable multitenancy support for our server, which is disabled by default. The easiest way to enable it is by adding this property to our application.yaml (or properties) file:

spring:
  security:
    oauth2:
      authorizationserver:
        multiple-issuers-allowed: true

5. Figuring Out the Current Tenant

Just enabling multiple issuers is not enough to fully enable multitenancy. If we carefully check SAS’s core components, they all implement interfaces that are not aware of the current tenant.

For instance, let’s look at the RegisteredClientRepository component, which has three methods:

public interface RegisteredClientRepository {
    void save(RegisteredClient registeredClient);
    RegisteredClient findById(String id);
    RegisteredClient findByClientId(String clientId);
}

Notice that none of them take a tenant identifier or something like that, which could be used by the implementation to find out which tenant the method should use.

So, how should we proceed? The solution lies in the AuthorizationServerContextFilter, an internal class that is always added to the SAS’s SecurityFilterChain.

This filter does two things:

  • Firstly, it extracts the issuer identifier from the current request, using the IssuerResolver helper class
  • Secondly, it creates an AuthorizationServerContext instance and uses AuthorizationServerContextHolder to bind it to the current thread.

Since this filter is called before other SAS filters, we can retrieve the issuer identifier inside a component whenever we need:

var issuer = AuthorizationServerContextHolder.getContext().getIssuer()

6. Multitenant-aware Components Implementation Strategy

Before we start implementing those components, we must define how we are going to load information for each tenant. For this tutorial, we’ll keep things simple and use a properties/YAML-based approach.

This is the @ConfigurationProperties class we’ll use to read the information of each tenant and store it in a map, indexed by its tenant identifier:

@ConfigurationProperties(prefix = "multitenant-auth-server")
public class MultitenantAuthServerProperties {

    private Map<String, OAuth2AuthorizationServerProperties> tenants = new HashMap<>();

    public Map<String, OAuth2AuthorizationServerProperties> getTenants() {
        return tenants;
    }

    public void setTenants(Map<String, OAuth2AuthorizationServerProperties> tenants) {
        this.tenants = tenants;
    }
}

The values in the tenants map use the library’s own OAuth2AuthorizationServerProperties to store tenant-specific information, such as the registered clients.

This is an example of an application.yaml file defining two tenants with one registered client on each. Notice that even if the clients use the same client-id value, they’ll still be treated as distinct clients.

multitenant-auth-server:
  tenants:
    issuer1:
      client:
        client1:
          require-authorization-consent: false
          registration:
            client-name: Client 1 - Issuer 1
            client-id: client1
            scopes:
              - openid
              - email
              - account:read
            # ... other properties omitted
    issuer2:
      client:
        client1:
          require-authorization-consent: false
          registration:
            client-name: 'Client 1 - Issuer 2'
            client-id: client1
            scopes:
              - openid
              - email
              - account:write
            # ... other properties omitted

The implementation strategy for the components will follow a composite delegate approach. At initialization time, each component receives a map of instances of the same kind indexed by tenant identifier. In each case, the component will implement the required functionality following a simple strategy:

  1. Retrieve the current issuer
  2. Map the issuer to a tenant identifier
  3. Use the tenant identifier to look up a matching delegate
  4. Call the appropriate delegate’s method
Since steps 1 to 3 are essentially the same regardless of the component type or called method, we’ll factor them out to a base class:
public class AbstractMultitenantComponent<T> {

    private Map<String,T> componentsByTenant;
    private Supplier<AuthorizationServerContext> authorizationServerContextSupplier;

    protected AbstractMultitenantComponent(Map<String,T> componentsByTenant,
      Supplier<AuthorizationServerContext> authorizationServerContextSupplier) {
        this.componentsByTenant = componentsByTenant;
        this.authorizationServerContextSupplier = authorizationServerContextSupplier;
    }

    protected Optional<T> getComponent() {

        var authorizationServerContext = authorizationServerContextSupplier.get();
        if (authorizationServerContext == null || authorizationServerContext.getIssuer() == null) {
            return Optional.empty();
        }

        var issuer = authorizationServerContext.getIssuer();
        for (var entry : componentsByTenant.entrySet()) {
            if (issuer.endsWith(entry.getKey())) {
                return Optional.of(entry.getValue());
            }
        }

        return Optional.empty();
    }
}

The getComponent() method contains the shared logic used by subclasses to get the tenant-specific component implementation to use with the current request. Notice that we’ve opted to use a Supplier<AuthorizationServerContext> instead of using AuthorizationServerContextHolder directly. The main reason is to simplify unit tests and to shield this logic from the strategy used to get the AuthorizationServerContext instance.

7. Core Components Implementation

Now, we’re ready to add multitenancy support for the following core SAS components:

  • RegisteredClientRepository
  • OAuth2AuthorizationService
  • OAuth2AuthorizationConsentService
  • JWKSource<SecurityContext>

Thanks to the functionality provided by the base class, the implementation is trivial. For instance, this is the full source for MultitenantRegisteredClientRepository, our multi-tenant-aware implementation for the RegisteredClientRepository component:

public class MultitenantRegisteredClientRepository
  extends AbstractMultitenantComponent<RegisteredClientRepository>
  implements RegisteredClientRepository  {

    public MultitenantRegisteredClientRepository(Map<String, RegisteredClientRepository> clientRepoByTenant,
      Supplier<AuthorizationServerContext> authorizationServerContextSupplier) {
        super(clientRepoByTenant,authorizationServerContextSupplier);
    }

    @Override
    public void save(RegisteredClient registeredClient) {
        getComponent()
          .orElseThrow(UnknownIssuerException::new)
          .save(registeredClient);
    }

    @Override
    public @Nullable RegisteredClient findById(String id) {
        return getComponent()
          .map(repo -> repo.findById(id))
          .orElse(null);
    }

    @Override
    public @Nullable RegisteredClient findByClientId(String clientId) {
        return getComponent()
          .map(repo -> repo.findByClientId(clientId))
          .orElse(null);
    }
}

The other implementation classes, available online, follow the same pattern.

8. Configuration

The next task is to register our components as @Bean instances, so SAS can use them instead of the regular ones.

AuthServerConfiguration is a @Configuration class that contains the required logic to build the delegate maps based on the provided properties and instantiate the multi-tenant-aware components.

A good example is the code that creates the OAuth2AuthorizationService bean:

@Bean
OAuth2AuthorizationService multitenantAuthorizationService(Supplier<AuthorizationServerContext> authorizationServerContextSupplier) {
    Map<String, OAuth2AuthorizationService> authServiceByTenant = new HashMap<>();
    for(var tenantId : multitenantAuthServerProperties.getTenants().keySet()) {
        authServiceByTenant.put(tenantId, new InMemoryOAuth2AuthorizationService());
    }
    return new MultitenantOAuth2AuthorizationService(authServiceByTenant,authorizationServerContextSupplier);
}

Here, we iterate over the tenant identifiers and, for each one, we create an InMemoryOAuth2AuthorizationService instance and put it in a new entry in the delegate map.

Once we’ve built the delegate map, we pass it to the multi-tenant aware component, together with the required Supplier<AuthorizationServerContext>.

9. Testing

Last, but not least, let’s write some tests to validate that we now have a multi-tenant-aware authorization server.

First, let’s check that our discovery endpoint is correctly handling requests that include a tenant identifier in the path:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MultitenantAuthServerApplicationUnitTest {

    @LocalServerPort
    private int port;

    @Test
    void whenRequestDiscoveryDocumentForIssuer1_thenSuccess() {
        restTestClient.get()
          .uri("/issuer1/.well-known/openid-configuration")
          .exchange()
          .expectStatus()
          .isOk()
          .expectBody()
          .jsonPath("$.issuer")
          .isEqualTo("http://localhost:" + port + "/issuer1");
    }
	
	// ... other tests omitted
}

This test starts the application using an ephemeral local port. We use a RestTestClient to make a GET request to the well-known location that returns the OIDC discovery document. Since we’ve added the issuer1 path element before the .well-known part, the returned JSON structure  must include it in the issuer property.

Next, let’s simulate a client_credentials token request for issuer1 using a valid client credentials and scope:

@Test
void givenClientCredentialsAndValidScope_whenRequestTokenForIssuer1_thenSuccess() {

    var response = restTestClient.post()
      .uri("/issuer1/oauth2/token")
      .header("Authorization", "Basic " + base64Encode("client1:secret1"))
      .header("Content-Type", "application/x-www-form-urlencoded")
      .body("grant_type=client_credentials&scope=account:read")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody()
      .jsonPath("$.access_token")
      .exists()
      .returnResult()
      .getResponseBodyContent();

     assertNotNull(response);
}

Let’s ensure that issuer1 clients can’t use a scope valid only for issuer2:

@Test
void givenClientCredentialsAndinalidScope_whenRequestTokenForIssuer1_thenError() {

    restTestClient.post()
      .uri("/issuer1/oauth2/token")
      .header("Authorization", "Basic " + base64Encode("client1:secret1"))
      .header("Content-Type", "application/x-www-form-urlencoded")
      .body("grant_type=client_credentials&scope=account:write") // Invalid scope for Tenant1
      .exchange()
      .expectStatus()
      .is4xxClientError();
}

Cool. As expected, we’ve got a 4xx error, which is what we want in this case.

10. Conclusion

In this tutorial, we’ve seen how to implement multitenancy in Spring Authorization Server.

By creating multi-tenant-aware components, we’ve added support for multiple tenants within a server instance.

As always, the full source code is available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – LSS – NPI (cat=Security/Spring Security)
announcement - icon

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security:

>> CHECK OUT THE COURSE

eBook Jackson – NPI EA – 3 (cat = Jackson)
guest
0 Comments
Oldest
Newest
Inline Feedbacks
View all comments