[JAVA] Multi-tenant with Keycloak

Introduction

I tried to realize multi-tenancy using Keycloak to use microservices in multiple projects.

What I tried

Looking at the Original Manual, it seems that it can be done by using the resolve () method of the KeycloakConfigResolver interface. However, I'd like you to forgive me for preparing Keycloak configs for multiple projects as Json files, and further investigation revealed that Keycloak configs can be dynamically generated with AdapterConfig as an argument. Since auth_server_url and resource are common, you can realize multi-tenancy by setting in application.yml and setting the project ID embedded in the request header to the realm name of Keycloak. However, I thought that generating Keycloak config for each request would be a performance problem, so I decided to cache it for a limited time with Guava's Cache Builder. ..

Source code

RequestBasedKeycloakConfigResolver.java


import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;

public class RequestBasedKeycloakConfigResolver implements KeycloakConfigResolver {
    
    private final Logger logger = LoggerFactory.getLogger(getClass());
    
    private final LoadingCache<String, KeycloakDeployment> keycloakDeploymentCache;
    
    public RequestBasedKeycloakConfigResolver() {
        keycloakDeploymentCache = CacheBuilder
                .newBuilder()
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build(
                        new CacheLoader<String, KeycloakDeployment>(){
                            @Override
                            public KeycloakDeployment load(String realm) throws Exception {
                                return loadKeycloakDeployment(realm);
                            }
                        }
                );
            
    }
    
    @Value("${keycloak.auth-server-url}")
    private String authServerUrl;
    
    @Value("${keycloak.resource}")
    private String resource;
    
    @Override
    public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
        String realm = request.getHeader("project-id");
        try {
            return keycloakDeploymentCache.get(realm);
        } catch (ExecutionException ex) {
            logger.error(ex.getMessage());
            return loadKeycloakDeployment(realm);
        }
    }
    
    private KeycloakDeployment loadKeycloakDeployment(String realm) {
        AdapterConfig cfg = new AdapterConfig();
        cfg.setRealm(realm);
        cfg.setAuthServerUrl(authServerUrl);
        cfg.setResource(resource);
        return KeycloakDeploymentBuilder.build(cfg);
    }
}

Summary

Since Keycloak can define the authentication method etc. for each realm, it was possible to support multiple projects by embedding the realm name in the request header and switching the realm for each request with KeycloakConfigResolver. Switching the Keycloak security settings for each request was a performance concern, so I decided to keep it in a time-limited cache for 10 minutes for the time being.

Reference material

Recommended Posts

Multi-tenant with Keycloak
Multi-project with Keycloak
Keycloak setup