February 25, 2025

Mastering Multi-Realm Authentication in NestJS with Keycloak

Introduction

In modern applications, especially those built for multi-tenancy, managing authentication across multiple Keycloak realms is a common requirement. Each realm can represent a different tenant, organization, or security boundary.

This guide will teach you how to configure NestJS with Keycloak to support multiple realms, implement guard enforcement policies, and perform token introspection dynamically based on different client_ids and roles.

By the end of this guide, you will:

  • ✅ Understand how Keycloak realms work
  • ✅ Learn how to integrate multiple realms dynamically
  • ✅ Implement role-based access control
  • ✅ Secure routes using Keycloak Guards
  • ✅ Perform token introspection with multiple client_ids and roles

🔹 What is a Keycloak Realm?

A realm in Keycloak is an isolated authentication domain. Each realm has its own users, roles, groups, and clients. This is useful for multi-tenancy, where different clients or organizations should have separate authentication policies.

Example Use Case:

  • CompanyA uses realm-a with client-1.
  • CompanyB uses realm-b with client-2.

In this scenario, authentication should be dynamically determined based on the request context.


Step 1: Install Dependencies

First, install the necessary packages to integrate Keycloak with NestJS.

npm install nest-keycloak-connect

Step 2: Create a Multi-Realm Configuration Service

Since nest-keycloak-connect expects a single realm configuration, we need a service that resolves realm and client dynamically.

MultiTenantKeycloakConfigService

Create a service that provides dynamic configuration based on the request.

import { Injectable, Scope, Request } from '@nestjs/common';
import {
  KeycloakOptionsFactory,
  KeycloakConnectOptions,
} from 'nest-keycloak-connect';

@Injectable({ scope: Scope.REQUEST }) // Per-request scope
export class MultiTenantKeycloakConfigService implements KeycloakOptionsFactory {
  private readonly realmConfigs = {
    tenant1: {
      realm: 'realm-1',
      clientId: 'client-1',
      secret: 'secret-1',
    },
    tenant2: {
      realm: 'realm-2',
      clientId: 'client-2',
      secret: 'secret-2',
    },
  };

  createKeycloakConnectOptions(@Request() req): KeycloakConnectOptions {
    const realmKey = this.getTenantFromRequest(req);
    const config = this.realmConfigs[realmKey];

    if (!config) {
      throw new Error(`No configuration found for tenant: ${realmKey}`);
    }

    return {
      authServerUrl: 'http://your-keycloak-server/auth',
      realm: config.realm,
      clientId: config.clientId,
      secret: config.secret,
      cookieKey: 'KEYCLOAK_JWT',
    };
  }

  private getTenantFromRequest(req): string {
    return req.headers['x-tenant-id'] || 'default';
  }
}

Step 3: Register Multi-Realm Configuration in app.module.ts

Modify AppModule to use the dynamic Keycloak configuration.

import { Module } from '@nestjs/common';
import { KeycloakConnectModule } from 'nest-keycloak-connect';
import { MultiTenantKeycloakConfigService } from './multi-tenant-keycloak-config.service';

@Module({
  imports: [
    KeycloakConnectModule.registerAsync({
      useClass: MultiTenantKeycloakConfigService,
    }),
  ],
})
export class AppModule {}

Step 4: Implement Keycloak Guards for Role-Based Access Control (RBAC)

Guards enforce authentication and authorization policies.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { KeycloakGuard } from 'nest-keycloak-connect';

@Injectable()
export class RoleGuard extends KeycloakGuard implements CanActivate {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) return true;
    const request = context.switchToHttp().getRequest();
    const userRoles = request.user.roles;
    return requiredRoles.some(role => userRoles.includes(role));
  }
}

Usage in Controllers:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from 'nest-keycloak-connect';
import { RoleGuard } from './role.guard';

@Controller('secure')
export class SecureController {
  @Get()
  @Roles('admin') // Restrict access to users with 'admin' role
  @UseGuards(RoleGuard)
  getSecureData() {
    return { message: 'You have accessed a protected route' };
  }
}

Step 5: Implement Token Introspection with Different Clients & Roles

Token introspection allows validating and extracting claims from tokens issued by different clients in multiple realms.

import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common';
import axios from 'axios';

@Injectable()
export class TokenIntrospectionGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];
    if (!token) return false;

    const clientConfig = this.getClientConfig(request.headers['x-tenant-id']);
    const introspectionUrl = `${clientConfig.authServerUrl}/realms/${clientConfig.realm}/protocol/openid-connect/token/introspect`;

    const response = await axios.post(introspectionUrl, {
      token,
      client_id: clientConfig.clientId,
      client_secret: clientConfig.secret,
    });

    return response.data.active;
  }

  private getClientConfig(tenant: string) {
    return { realm: 'realm-1', clientId: 'client-1', secret: 'secret-1', authServerUrl: 'http://your-keycloak-server/auth' };
  }
}

Conclusion

Dynamic Multi-Realm Authentication is implemented ✅ Guards enforce role-based accessToken introspection supports multiple clients

This approach enables a flexible, scalable, and secure multi-tenant authentication system. 🚀