How to Implement Multitenancy With Spring Boot and Keycloak
Secure your web application using different Keycloak realms in a single Keycloak instance. 1. Introduction In some cases, we need to secur...
Secure your web application using different Keycloak realms in a single Keycloak instance.
1. Introduction
In some cases, we need to secure a single web application with different realms. This concept is called multi-tenancy. The realms can be located on a single or different Keycloak instance. While it's easier to have a single realm per app, it could be costly as you have to host them on different servers.
Keycloak makes it possible by offering a config resolver that can be customized and where the Keycloak adapter config can be loaded and initialized. The configuration file from different realms will be saved in the project and loaded depending on a parameter. The realm name can be defined in the query parameter, header, or for this blog in the path parameter.
Clone this repository https://github.com/czetsuya/spring-keycloak-multi-tenant before proceeding.
2. Setting Up Keycloak
For this exercise, we will use a docker image. Needless to say, you must have a docker engine installed on your local machine. See the reference section below.
To run Keycloak, execute the following command in your terminal.
docker run --name=czetsuyatech-kc -p 8081:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin jboss/keycloak:11.0.3
Fire up your browser and navigate to http://localhost:8081, since we are forwarding that port to the container's 8080 where Keycloak is running.
2.1 Importing the Realms
Hover on Select realm, click Add realm. Click Select a file and import the file in the cloned repository at keycloak-realms/branch1-realm.json. Click Create.
Repeat the step with branch2-realm.json.
2.2 Roles and Users
Select Branch1. Click Roles. Make sure that you have a role with name ROLE_CATALOG_MANAGER. Otherwise, click Add Role.
2.3 Keycloak Clients
3. The Spring Boot Project
There are 3 classes of importance in the code. Let's check each of them.
1. Where we load the Keycloak configuration file depending on the path.public class PathBasedConfigResolver implements KeycloakConfigResolver { private final ConcurrentHashMap<String, KeycloakDeployment> cache = new ConcurrentHashMap<>(); @SuppressWarnings("unused") private static AdapterConfig adapterConfig; @Override public KeycloakDeployment resolve(OIDCHttpFacade.Request request) { String path = request.getURI(); int multitenantIndex = path.indexOf("tenant/"); if (multitenantIndex == -1) { throw new IllegalStateException("Not able to resolve realm from the request path!"); } String realm = path.substring(path.indexOf("tenant/")).split("/")[1]; if (realm.contains("?")) { realm = realm.split("\\?")[0]; } if (!cache.containsKey(realm)) { InputStream is = getClass().getResourceAsStream("/" + realm + "-keycloak.json"); cache.put(realm, KeycloakDeploymentBuilder.build(is)); } return cache.get(realm); } static void setAdapterConfig(AdapterConfig adapterConfig) { PathBasedConfigResolver.adapterConfig = adapterConfig; } }Note: This class must be produce outside the KeycloakConfiguration annotation so that it is generated first. So in your SpringBootApplication annotated class add.
// SpringBootApplication annotated class @Bean @ConditionalOnMissingBean(PathBasedConfigResolver.class) public KeycloakConfigResolver keycloakConfigResolver() { return new PathBasedConfigResolver(); } //KeycloakConfiguration annotated class, annotate with @DependsOn("keycloakConfigResolver")2. Set the correct redirect URL as by default spring redirects to /sso/login. We've change it to /tenant/{realmName}/sso/login by overriding the class KeycloakAuthenticationEntryPoint.
public class MultitenantKeycloakAuthenticationEntryPoint extends KeycloakAuthenticationEntryPoint { public MultitenantKeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext) { super(adapterDeploymentContext); } public MultitenantKeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext, RequestMatcher apiRequestMatcher) { super(adapterDeploymentContext, apiRequestMatcher); } @Override protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { String path = request.getRequestURI(); int multitenantIndex = path.indexOf("tenant/"); if (multitenantIndex == -1) { throw new IllegalStateException("Not able to resolve realm from the request path!"); } String realm = path.substring(path.indexOf("tenant/")).split("/")[1]; if (realm.contains("?")) { realm = realm.split("\\?")[0]; } String contextAwareLoginUri = request.getContextPath() + "/tenant/" + realm + DEFAULT_LOGIN_URI; response.sendRedirect(contextAwareLoginUri); } }3. The KeycloakConfigurationAdapter which overrides KeycloakWebSecurityConfigurerAdapter. It's important to point out that we overriden the keycloakAuthenticationProcessingFilter filter.
@Override protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception { KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter( // authenticationManager(), // new AntPathRequestMatcher("/tenant/*/sso/login")); filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy()); return filter; }
3 comments
Thank you! Decided to use your approach of multi-tenancy. But get a redirect loop where i do not get get on sso/login page and authenticate. Could you please advice something what should i look up to?
What do you have in your logs? Both Keycloak and Spring? Were you able to reach Keycloak at all?
Hi Czetsuya,
Thank you so much for your design here. I have implemented it in my application which has a ReactJS frontend to it. I'm redirecting the control to the ReactJS app once the login is successful.
Do I have a manually set the cookies with the token and userId in the HTTPResponse back to the frontend?
I have manually set the user-access-token to the HTTPResponse to frontend, but that is in "HTTPOnly" and not able to access by my frontend.
I want to call the other keycloak Server APIs like get-users, add-users etc with the role and permissions of the loggedIn user. How can I achieve this?
Post a Comment