Multi tenancy with Keycloak and Spring Boot OAuth 2 Client

Pavith Madusara
4 min readNov 15, 2022

--

When building an application that supports multiple tenants in order to track what tenant each user or request belongs to, we have to use a unique id for each tenant called tenant id. Sending the X-Tenant-Id header is the simplest way of handling this but is the most vulnerable method. So, adding tenant id to the JWT token as a claim is the most common way of handling it.

In this article, I’m going to explain how we can achieve the same result with Spring Boot Keycloak Adapter. In this scenario, our spring boot application will be the resource server.
Keycloak supports multi-tenancy by supporting multiple realms. However, it depends on our own requirements if we are able to create a realm per tenant or not. If we can’t do that, we can store the tenant id as a user attribute. The guide in this article can be used for both scenarios. I’m going to explain how we can do this using user attributes because it has the most steps to go through. If you want to go with ream per tenant approach, in the step we get the tenant id from Keycloak, you can simply get the realm and continue with it.

Keycloak Setup

Please note that i am using Keycloak version 20.0.1

  1. Log in to Keycloak Administration Console and Select your realm. Then create a OpenID Connect type client. This will be our spring boot application.

2. Navigate to Client Scopes and Create a new client scope for Tenant ID

3. Navigate to Mappers tab and create a Mapper to include Tenant ID in the JWT Token. Make sure to enable the Add to access token option.

Done. You are now ready to use tenantId user attribute. You have to make sure to add attributes when creating a user. For testing, you can go ahead and create a user and manually add the attribute. To Make sure every ting works correctly you can sign in to your test user account using postman and inspect the content of the JWT. It should contain a claim called tenantId with the value you have assigned to the user.

Spring Boot Setup

  1. Add dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>19.0.3</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>19.0.3</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

2 . Keycloak Security Config

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class KeyCloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
KeycloakRestTemplate restTemplate = new KeycloakRestTemplate(keycloakClientRequestFactory);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
return restTemplate;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.anyRequest()
.authenticated();
http.csrf().disable();
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}

@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
}

3. OAuth 2 Web Client Config

@Configuration
public class OAuth2WebClientConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService clientService) {

OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();

AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, clientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

return authorizedClientManager;
}

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("keycloak");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
}

4. Update Application Properties

keycloak:
realm: ${AUTH_REALM}
resource: ${AUTH_RESOURCE}
auth-server-url: ${AUTH_SERVER}
credentials:
secret: ${AUTH_SECRET}
bearer-only: true

spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: ${keycloak.resource}
client-secret: ${keycloak.credentials.secret}
authorization-grant-type: client_credentials
provider:
keycloak:
token-uri: ${keycloak.auth-server-url}/realms/${keycloak.realm}/protocol/openid-connect/token

5. Create Tenant Context to Store and get the tenantId as needed. (This implementation can be optimized depending on the requirement and the architecture you have already implemented in the application.)

@Slf4j
public final class TenantContext {
public static Integer getTenantId() {
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext != null && securityContext.getAuthentication() != null) {
Object principal = securityContext.getAuthentication().getPrincipal();
if (principal instanceof KeycloakPrincipal<?> keycloakPrincipal) {
Object tenantId = keycloakPrincipal.getKeycloakSecurityContext().getToken().getOtherClaims().get("tenantId");
if (tenantId instanceof Integer) {
return (Integer) tenantId;
} else {
log.error("TenantId is not an Integer");
throw new RuntimeException("TenantId is not an Integer or null");
}
} else {
log.warn("Principal is not a KeycloakPrincipal");
throw new RuntimeException("Principal is not KeycloakPrincipal");
}
} else {
log.warn("Security Context is null");
throw new RuntimeException("Security Context is null");
}
}
}

Done ! Now you are ready to use Tenant Context. You can implement a Entity Listener for adding tenantId to your entities.

@EntityListeners({TenantEntityListener.class})
public abstract class BaseEntity implements TenantAware {
@Column
private Integer tenantId;
}
public interface TenantAware {
Integer getTenantId();
void setTenantId(Integer tenantId);
}
public class TenantEntityListener {
@PrePersist
@PreUpdate
@PreRemove
public void prePersist(Object entity) {
if (entity instanceof TenantAware tenantAware) {
tenantAware.setTenantId(TenantContext.getTenantId());
}
}
}

See Full Multi Tenant Implementation with PostgreSQL Row Level Security Example at my Git Hub Repository

Happy Coding !

--

--

No responses yet