DEV Community

Cover image for Implementing Multi-tenancy with Keycloak and NestJS
Ítalo Queiroz
Ítalo Queiroz

Posted on

1

Implementing Multi-tenancy with Keycloak and NestJS

Hey devs! 👋

If you've ever found yourself scratching your head trying to implement a system where different organizations (or clients) need their own isolated space, with their own users and configurations, you're probably dealing with multi-tenancy. In this article, I'll share a practical approach to implementing this using Keycloak and NestJS.


The Problem

Imagine you're building a SaaS and each client needs:

  • Their own isolated environment
  • Manage their own users
  • Have their own authentication settings
  • Be able to use different identity providers

Sounds complex? Well, that's where our solution with Keycloak and NestJS comes in!


Why Keycloak?

Keycloak is a powerful Identity and Access Management (IAM) tool that provides native support for multi-tenancy through "realms". Each realm is like an isolated mini authentication server, with its own settings, users, and clients.


The Solution

Let's break down our implementation into main parts:

1. Dynamic Realm Creation

First, we need a service that will manage our realms in Keycloak. Here's a simplified example:

@Injectable()
class KeycloakService {
  private kcAdminClient: KeycloakAdminClient;

  constructor(private configService: ConfigService) {
    this.kcAdminClient = new KeycloakAdminClient({
      baseUrl: this.configService.get('keycloak.url'),
      realmName: 'master'
    });
  }

  async createRealm(config: {
    organizationName: string;
    displayName: string;
    theme?: string;
  }) {
    await this.authenticate();

    const realmConfig = {
      realm: config.organizationName,
      displayName: config.displayName,
      enabled: true,
      sslRequired: 'external',
      loginTheme: config.theme || 'default',
      // Other security settings...
    };

    await this.kcAdminClient.realms.create(realmConfig);

    // Configure OAuth2 clients, default roles, etc...
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Tenant Identification Middleware

In each request, we need to identify which tenant (organization) is making the call. A common approach is to use a custom header:

@Injectable()
export class TenantMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    const organizationId = req.headers['x-organization-id'];

    if (!organizationId) {
      throw new UnauthorizedException('Organization ID is required');
    }

    req.tenantId = organizationId;
    next();
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Token Validation per Tenant

Each tenant has their own realm in Keycloak, so we need to validate tokens considering this:

export async function validateToken(token: string, organizationId: string) {
  const jwksClient = getJwksClient(organizationId);

  try {
    const decodedToken = jwt.decode(token, { complete: true });
    const key = await jwksClient.getSigningKey(decodedToken.header.kid);

    return jwt.verify(token, key.getPublicKey(), {
      algorithms: ['RS256'],
      issuer: `${KEYCLOAK_URL}/realms/${organizationId}`
    });
  } catch (error) {
    throw new UnauthorizedException('Invalid token');
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Keycloak Context Decorator

To facilitate access to the Keycloak admin client in the current tenant context:

export const KeycloakContext = createParamDecorator(
  async (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const organizationId = request.tenantId;

    const adminClient = new KeycloakAdminClient({
      baseUrl: KEYCLOAK_URL
    });

    await adminClient.auth({
      grantType: 'client_credentials',
      clientId: 'admin-cli',
      clientSecret: ADMIN_SECRET
    });

    adminClient.setConfig({ realmName: organizationId });

    return {
      adminClient,
      realm: organizationId
    };
  }
);
Enter fullscreen mode Exit fullscreen mode

5. Using in a Controller

Now we can put it all together in a controller:

@Controller('users')
export class UserController {
  @Post()
  async createUser(
    @KeycloakContext() kc: KeycloakContext,
    @Body() userData: CreateUserDto
  ) {
    const { adminClient, realm } = kc;

    // Create user in the tenant-specific realm
    const user = await adminClient.users.create({
      realm,
      username: userData.email,
      email: userData.email,
      enabled: true,
      // other properties...
    });

    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Tips and Considerations

  1. Smart Caching: Implement caching for JWT tokens and user information, but remember to separate by tenant!

  2. Data Migration: Have a clear strategy for data migration when creating new tenants.

  3. Monitoring: Add detailed logs to debug tenant-specific issues.

  4. Testing: Create tests that validate isolation between tenants.


Conclusion

Implementing multi-tenancy might seem daunting at first, but with the right tools (like Keycloak and NestJS) and a well-thought-out architecture, we can create a robust and scalable solution.

The code I showed here is just the tip of the iceberg, there's much more to consider like per-tenant database management, distributed caching, per-organization rate limiting, etc. But that's a topic for another article! 😉

SurveyJS custom survey software

Simplify data collection in your JS app with a fully integrated form management platform. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more. Integrates with any backend system, giving you full control over your data and no user limits.

Learn more

Top comments (1)

Collapse
 
bhu profile image
van

How do you handle multi-tenancy? With multiple realms, clients, or organizations?

JavaScript-ready auth and billing that just works

JavaScript-ready auth and billing that just works

Stop building auth from scratch. Kinde handles authentication, user management, and billing so you can focus on what matters - shipping great products your users will love.

Get a free account

Announcing the First DEV Education Track: "Build Apps with Google AI Studio"

The moment is here! We recently announced DEV Education Tracks, our new initiative to bring you structured learning paths directly from industry experts.

Dive in and Learn

DEV is bringing Education Tracks to the community. Dismiss if you're not interested. ❤️