Implementing Multi-Tenant Security with Spring Boot and Spring Security
Multi-tenancy is a software architecture where a single application instance serves multiple tenants (clients), each with its own data and configurations. In a multi-tenant system, security is critical — we must ensure that one tenant cannot access the data or resources of another.
This guide walks you through implementing tenant-aware authentication and authorization using Spring Boot and Spring Security, leveraging filter chains to isolate and enforce tenant boundaries.
1. Key Concepts
Before diving into the implementation, let’s clarify the core concepts:
- Tenant: A distinct client or customer, typically identified by a domain, subdomain, or request header.
- Authentication: Verifying who the user is.
- Authorization: Determining what the authenticated user can access.
- Filter Chain: A sequence of filters that process the request before it reaches the controller.
2. Architecture Overview
We’ll build the following components:
- Tenant Resolver: Resolves tenant information from the incoming request.
- Custom Authentication Filter: Validates user credentials in a tenant-aware manner.
- Custom Security Filter Chain: Applies tenant-specific rules and separates concerns.
- Tenant Context Holder: Stores tenant info per request.
- Data Isolation (optional): Ensures tenant-specific data access.
3. Step-by-Step Implementation
1. Project Setup
Add the following dependencies in build.gradle or pom.xml:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
2. Tenant Context Holder
We use a ThreadLocal to store tenant info per request lifecycle:
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
currentTenant.set(tenantId);
}
public static String getTenantId() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
3. Tenant Resolver Filter
Extract the tenant ID from headers or subdomains.
@Component
public class TenantResolverFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String tenantId = request.getHeader("X-Tenant-ID");
if (tenantId != null && !tenantId.isEmpty()) {
TenantContext.setTenantId(tenantId);
}
try {
filterChain.doFilter(request, response);
} finally {
TenantContext.clear(); // prevent memory leaks
}
}
}
4. Custom Authentication Filter
A simplified filter that handles tenant-specific authentication logic:
public class TenantAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public TenantAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl("/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
String tenantId = TenantContext.getTenantId();
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username + "|" + tenantId, password);
return authenticationManager.authenticate(authToken);
}
}
5. Custom Authentication Provider
Handle tenant-aware logic in the provider:
@Component
public class TenantAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String[] parts = ((String) authentication.getPrincipal()).split("\\|");
String username = parts[0];
String tenantId = parts[1];
String password = (String) authentication.getCredentials();
// Fetch user based on username and tenantId from DB
if ("user".equals(username) && "tenant1".equals(tenantId) && "pass".equals(password)) {
return new UsernamePasswordAuthenticationToken(username, password,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
}
throw new BadCredentialsException("Invalid credentials for tenant: " + tenantId);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
6. Configure Spring Security
Register filters and authentication provider:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private TenantAuthenticationProvider tenantAuthProvider;
@Autowired
private TenantResolverFilter tenantResolverFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
AuthenticationManager authManager = authenticationManager(http);
http
.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(tenantResolverFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new TenantAuthenticationFilter(authManager), UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(tenantAuthProvider);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.authenticationProvider(tenantAuthProvider)
.build();
}
}
🧪 Sample Request
curl -X POST http://localhost:8080/login \ -H "X-Tenant-ID: tenant1" \ -d "username=user&password=pass"
4. Data Isolation Strategy (Optional but Recommended)
If you’re using a database per tenant or shared DB with tenant IDs:
- Use Hibernate’s
@FilterDefor Spring Data interceptors. - Use multi-tenant JPA configuration using
AbstractRoutingDataSource.
5. Best Practices
- Validate tenant existence early in the filter.
- Avoid global static context outside of request scope.
- Log tenant ID in request/response logs.
- Secure default tenant fallback behavior (e.g., reject if tenant ID is missing).
6. Extensions
- Integrate with OAuth2 or JWT for stateless multi-tenant security.
- Use Spring Cloud Gateway for subdomain-based tenant routing.
- Add a
TenantDetailsServicefor loading user data per tenant.
7. Conclusion
Multi-tenant security requires isolating not only data but also authentication and authorization logic per tenant. By leveraging custom filters, Spring Security, and a clean tenant context design, you can build secure, scalable applications that serve multiple clients reliably.




