I tried to realize multi-tenancy using Keycloak to use microservices in multiple projects.
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. ..
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);
}
}
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.