Multi-tenancy is a critical aspect of contemporary software architecture. It assists in overcoming significant difficulties, particularly for SaaS software. Multi-tenancy impacts various application layers, ranging from the database to the front-end. Authentication is one of the sectors significantly impacted, and efficient authentication management is crucial for SaaS software. This paper presents an illustration of multi-tenant authentication by implementing Keycloak on an Angular Springboot stack.

To propose an implementation, we will present a use case that allows us to define the requirements. We will describe the functional and technical context in which we will operate, and then specify the requirements. Based on these requirements, we will propose a Keycloak implementation to meet them and make the necessary adaptations on the Angular and Springboot side.

Environment

Functional context

This concerns an accountancy firm that provides services to external clients and has employed staff to manage the files. If a customer (external user) wishes to connect, they must create an account on the Saas application. In the same manner, when the staff (internal user) desire to work on the files, they must use their Active Directory account to log in.

It is important to consider that customers and employees may share some rights, but also have distinct ones. The two databases must not be affected, and any changes made to internal users should not affect customers.

Technical context

The existing Saas product is divided into three components Frontend, Backend and database.

Keycloak

Authentication will utilize the OAuth2 protocol in OpenID Connect. Keycloak satisfies these and other requirements.

Architecture

One possible solution could be to have two entirely standalone Keycloak instances, which could lead to higher maintenance and infrastructure costs. Therefore, we will investigate the possibility of using a single instance of Keycloak.

NB: The roles could also be implemented at a realm level and a client level for greater precision.

Deployments

To make deployment easier, we’re going to use a docker-compose:

version: ’3’
services:
keycloak:
image:
quay.io/keycloak/keycloak:22.0.1
ports:
- "8080:8080"
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
command: ["start-dev"]

You can deploy your application just using ’docker-compose up -d’. Then create the two realms, no special configurations are required. Then create a client-front and client-back in each realm. For the client-front, you do not need to modify the default realm. For client-back, you will have to set ’Client authentication’ to ’On’.

Components adaptation

Now that we have Keycloak installed and configured, we need to customize the components.

Frontend

For the front-end, we consider a simple Angular application.

{
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
multi: true,
deps: [KeycloakService,
KeycloakConfigService]
},

Declaration of ’initialiseKeycloak’ method:

export function initializeKeycloak(
    keycloak: KeycloakService,
    keycloakConfigService:
        KeycloakConfigService
) {
// Set default realm
    let realm = "EXTERNAL";
    const pathName: string[] =
        window.location.pathname.split('/');
    if (pathName[1] === "EXTERNAL") {
        realm = "EXTERNAL";
    }
    if (pathName[1] === "INTERNAL") {
        realm = "INTERNAL";
    }
    return (): Promise<any> => {
        return new Promise<any>(async (resolve, reject) => {
            try {
                await initMultiTenant(keycloak, keycloakConfigService, realm);
                resolve(auth);
            } catch (error) {
                reject(error);
            }
        });
    };
}

export async function initMultiTenant(
    keycloak: KeycloakService,
    keycloakConfigService:
        KeycloakConfigService,
    realm: string
) {
    return keycloak.init({
        config: {
            url: await
                firstValueFrom(keycloakConfigService
                    .fetchConfig()).then(
                    (conf: PublicKeycloakConfig) => {
                        return conf?.url;
                    }
                ),
            realm,
            clientId: 'front-client'
        },
        initOptions: {
            onLoad: 'login-required',
            checkLoginIframe: false
        },
        enableBearerInterceptor: true,
        bearerExcludedUrls:
            ['/public-configuration/keycloak']
    });
}

Backend

In the backend, we should intercept incoming requests in order to: 1. Get the current realm to contact keycloak on the appropriate configuration. 2. Based on the previous realm, contact keycloak to validate the bearer token.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>
        spring-boot-starter-security
    </artifactId>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>
        keycloak-spring-boot-starter
    </artifactId>
    <version>18.0.2</version>
</dependency>
<dependency>
    <groupId>org.keycloak.bom</groupId>
    <artifactId>keycloak-adapter-bom</artifactId>
    <version>18.0.2</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

    @Override
    public KeycloakDeployment
    resolve(OIDCHttpFacade.Request request) {
        String header =
                request.getHeaders(CUSTOM_HEADER_REALM_SELECTOR)
                        .stream().findFirst().orElse(null);
        if (EXT_XEN_REALM.equals(header)) {
            buildAdapterConfig(extXenKeycloakConfig);
        } else {
            buildAdapterConfig(intXenKeycloakConfig);
        }
        return
                KeycloakDeploymentBuilder.build(adapterConfig);
    }

Conclusion

Finally, we obtain the following architecture:

Final architecture

By following these steps, we can ensure that a user lands on the correct login page and navigates through the application independently of their realm, all controlled by a single instance of Keycloak.