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_id
s 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_id
s 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 withclient-1
.CompanyB
uses realm-b withclient-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 access ✅ Token introspection supports multiple clients
This approach enables a flexible, scalable, and secure multi-tenant authentication system. 🚀