no

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.


If no roles nor users exist, that's fine we just need to create a user for each realm.

2.3 Keycloak Clients

Select Branch1. Click Clients. Make sure that you have a client with Client Id "web".


Otherwise, click create and enter web in the Client ID field.


Inside the client, check and set if not properly configured:
  • Access Type=public
  • Valid Redirect URIs=*

3. The Spring Boot Project

If you have successfully cloned the project at  https://github.com/czetsuya/spring-keycloak-multi-tenant in Sprint STS, it should look like this:

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;
}

4. Testing

To run the app, right-click on the project click Debug As / Spring Boot App.

You should see these logs in your console.

Open your web browser and navigate to http://localhost:8082/tenant/branch1/catalog. If you configured an Identity Provider and set it as the value in idpHint then it should automatically redirect.



Otherwise, it will show the Keycloak login screen. If this is the case, you need to create a user with the role ROLE_CATALOG_MANAGER first.

If login is successful, it should print your firstName, lastName, & bearer-token in the console and output the subjectId in the browser, which basically is the user id inside Keycloak.


5. Summary

In this blog, we learn how to use Keycloak in a multi-tenant setup. Which is usually the requirement in a SAAS application with multiple customers. We use the Spring Boot Keycloak adapter and Spring Security features to load the appropriate realm from the URL path and client that match our tenant which contains its own set of users and roles.

References

Related

spring-cloud 3762710256509761887

Post a Comment Default Comments

3 comments

Unknown said...

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?

czetsuya said...

What do you have in your logs? Both Keycloak and Spring? Were you able to reach Keycloak at all?

jehoshuah said...

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?

item