Building a Multi-Tenant SSO Integration with Spring Boot
Introduction
Single Sign-On (SSO) is an authentication process that allows users to access multiple applications using a single set of login credentials. It improves security and user experience, simplifying the management of multiple accounts across various services. This article will guide you through the process of building a multi-tenant enterprise SSO integration using Spring Boot that can integrate with any SSO provider like Google, Outlook, and others for different customers. We’ll provide detailed explanations and code samples for a better understanding of the process.
Prerequisites
Before we dive in, ensure you have the following installed and set up:
- Java Development Kit (JDK) 8 or later
- Apache Maven
- A text editor or Integrated Development Environment (IDE) of your choice
- A basic understanding of Spring Boot, OAuth 2.0, and OpenID Connect (OIDC)
Getting Started with Spring Boot
- Create a new Spring Boot project using Spring Initializr. Select the following dependencies:
- Web: “Spring Web”
- Security: “Spring Security”
- OAuth 2.0 Client: “Spring Boot OAuth2 Client”
2. Download and extract the generated project.
3. Open the project in your preferred IDE and make sure all dependencies are properly loaded.
Implementing the SSO Integration
We will use OAuth 2.0 and OpenID Connect (OIDC) to integrate with external SSO providers. OIDC is an identity layer built on top of OAuth 2.0, which provides authentication and authorization features. Follow the steps below to implement the multi-tenant SSO integration.
- Update the
application.properties
file:
# Base properties for SSO integration
spring.security.oauth2.client.registration.dynamic.registration-id=<your_registration_id>
spring.security.oauth2.client.registration.dynamic.scope=openid,email,profile
# These properties will be dynamically overridden for each tenant
spring.security.oauth2.client.registration.dynamic.client-id=<your_client_id>
spring.security.oauth2.client.registration.dynamic.client-secret=<your_client_secret>
spring.security.oauth2.client.registration.dynamic.provider=<your_sso_provider>
Replace <your_registration_id>
, <your_client_id>
, <your_client_secret>
, and <your_sso_provider>
with the actual values obtained from the SSO provider.
2. Create a new class DynamicOAuth2ClientRegistration
to store the dynamic client registration details:
package com.example.sso;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
public class DynamicOAuth2ClientRegistration {
private String tenantId;
private ClientRegistration clientRegistration;
// Constructor, getters, and setters
}
3. Create a new class DynamicOAuth2ClientRegistrationRepository
that implements ClientRegistrationRepository
:
package com.example.sso;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import java.util.HashMap;
import java.util.Map;
public class DynamicOAuth2ClientRegistrationRepository implements ClientRegistrationRepository {
private final Map<String, DynamicOAuth2ClientRegistration> dynamicClientRegistrations;
public DynamicOAuth2ClientRegistrationRepository() {
this.dynamicClientRegistrations = new HashMap<>();
}
@Override
public ClientRegistration findByRegistrationId(String registrationId) {
// Retrieve the client registration for the given tenant
DynamicOAuth2ClientRegistration dynamicRegistration = dynamicClientRegistrations.get(registrationId);
return dynamicRegistration == null ? null : dynamicRegistration.getClientRegistration();
}
// Additional methods for managing dynamic client registrations
}
4. Modify the WebSecurityConfig
class to configure the DynamicOAuth2ClientRegistrationRepository
bean and inject it into the HttpSecurity
configuration:
package com.example.sso;
// ...other imports...
import org.springframework.context.annotation.Bean;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public DynamicOAuth2ClientRegistrationRepository dynamicClientRegistrationRepository() {
return new DynamicOAuth2ClientRegistrationRepository();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// ...existing code...
.and()
.oauth2Login()
.clientRegistrationRepository(dynamicClientRegistrationRepository())
.defaultSuccessURL("/user", true);
}
}
5. Create a new class Tenant
to represent each tenant's information:
package com.example.sso;
public class Tenant {
private String id;
private String name;
private String ssoProvider;
private String clientId;
private String clientSecret;
// Constructor, getters, and setters
}
6. Add a new class TenantRepository
to store and manage tenant information:
package com.example.sso;
import java.util.List;
public interface TenantRepository {
Tenant findById(String id);
List<Tenant> findAll();
Tenant save(Tenant tenant);
void deleteById(String id);
}
You may implement TenantRepository
using your preferred database technology (e.g., JPA, JDBC, MongoDB, etc.). This article does not cover specific database implementation details, but you can find more information in the Spring Data documentation.
7. Create a new class TenantService
to handle tenant-specific logic:
package com.example.sso;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TenantService {
@Autowired
private TenantRepository tenantRepository;
public Tenant findById(String id) {
return tenantRepository.findById(id);
}
public List<Tenant> findAll() {
return tenantRepository.findAll();
}
public Tenant save(Tenant tenant) {
return tenantRepository.save(tenant);
}
public void deleteById(String id) {
tenantRepository.deleteById(id);
}
}
8. Update the DynamicOAuth2ClientRegistrationRepository
class to inject TenantService
and retrieve the tenant-specific client registration details:
package com.example.sso;
// ...other imports...
import org.springframework.beans.factory.annotation.Autowired;
public class DynamicOAuth2ClientRegistrationRepository implements ClientRegistrationRepository {
// ...existing code...
@Autowired
private TenantService tenantService;
@Override
public ClientRegistration findByRegistrationId(String registrationId) {
Tenant tenant = tenantService.findById(registrationId);
if (tenant == null) {
return null;
}
// Create a dynamic client registration based on the tenant's SSO provider
DynamicOAuth2ClientRegistration dynamicRegistration = createDynamicClientRegistration(tenant);
return dynamicRegistration == null ? null : dynamicRegistration.getClientRegistration();
}
// ...other methods...
private DynamicOAuth2ClientRegistration createDynamicClientRegistration(Tenant tenant) {
// Retrieve the OAuth2 provider details based on the tenant's SSO provider
// and create a dynamic client registration using the tenant's client ID and secret
}
}
9. Update the WebSecurityConfig
class to configure the DynamicOAuth2ClientRegistrationRepository
bean and inject it into the HttpSecurity
configuration:
package com.example.sso;
// ...other imports...
import org.springframework.context.annotation.Bean;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public DynamicOAuth2ClientRegistrationRepository dynamicClientRegistrationRepository() {
return new DynamicOAuth2ClientRegistrationRepository();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// ...existing code...
.and()
.oauth2Login()
.clientRegistrationRepository(dynamicClientRegistrationRepository())
.defaultSuccessURL("/user", true);
}
}
Now, your application is ready to handle multi-tenant SSO integrations with different providers. You can store each tenant's SSO provider, client ID, and client secret in the database and dynamically retrieve the client registration details based on the current tenant.
Testing the Multi-Tenant SSO Integration
To test the multi-tenant SSO integration, you can create a simple web application with a login page that displays the available SSO providers for each tenant. Perform the following steps:
- Update the
application.properties
file to include the Thymeleaf dependency:
spring.thymeleaf.cache=false
2. Add the Thymeleaf dependency to your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
3. Create a new controller class LoginController
:
package com.example.sso;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class LoginController {
@Autowired
private TenantService tenantService;
@GetMapping("/login/{tenantId}")
public String login(@PathVariable("tenantId") String tenantId, Model model) {
Tenant tenant = tenantService.findById(tenantId);
if (tenant == null) {
return "error";
}
model.addAttribute("tenant", tenant);
return "login";
}
}
4. Create a new Thymeleaf template src/main/resources/templates/login.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<h1>Login with SSO Provider</h1>
<div th:if="${tenant.ssoProvider == 'google'}">
<a th:href="@{/oauth2/authorization/{tenantId}(tenantId=${tenant.id})}">Login with Google</a>
</div>
<div th:if="${tenant.ssoProvider == 'outlook'}">
<a th:href="@{/oauth2/authorization/{tenantId}(tenantId=${tenant.id})}">Login with Outlook</a>
</div>
<!-- Add other SSO providers as needed -->
</body>
</html>
Now, you can run your application and test the multi-tenant SSO integration by visiting the /login/{tenantId}
URL with the corresponding tenant ID. After successful authentication, the user will be redirected to the /user
URL.
Conclusion
In this article, we have demonstrated how to build a multi-tenant enterprise SSO integration with Spring Boot that can integrate with different SSO providers like Google, Outlook, Okta, and others. The implementation uses OAuth 2.0 and OpenID Connect (OIDC) to provide authentication and authorization features. We have covered the required configuration, database design.
🔗 Connect with me on LinkedIn!
I hope you found this article helpful! If you’re interested in learning more and staying up-to-date with my latest insights and articles, don’t hesitate to connect with me on LinkedIn.
Let’s grow our networks, engage in meaningful discussions, and share our experiences in the world of software development and beyond. Looking forward to connecting with you! 😊