NestJS Authentication with Keycloak: Complete Guide
Last updated: March 2026
NestJS is a natural pairing for Keycloak. Both are enterprise-oriented, both embrace modular architecture, and together they give you a TypeScript API with single sign-on, role-based access control, and multi-factor authentication without building any of the identity plumbing yourself.
This guide covers two approaches to integrating Keycloak with NestJS:
- nest-keycloak-connect — a purpose-built Keycloak module with guards and decorators
- Custom JWT validation using
@nestjs/passportandpassport-jwt— for teams that want more control
Both approaches produce the same result: a NestJS API where routes are protected by Keycloak-issued JWTs, and access decisions are made based on realm and client roles.
Prerequisites
- Node.js 20+ and npm 10+
- NestJS 10+ (NestJS CLI recommended)
- A running Keycloak instance (version 22+). Use our Docker Compose Generator for local setup or try managed Keycloak hosting.
- TypeScript familiarity
Step 1: Configure the Keycloak Client
In the Keycloak Admin Console:
- Go to Clients > Create client
- Set Client ID to
nestjs-api - Set Client type to
OpenID Connect - Enable Client authentication (confidential client)
- Enable Service accounts roles (for backend-to-backend auth)
- Set Valid redirect URIs to
http://localhost:3000/* - Set Web origins to
http://localhost:3000
After saving, copy the Client secret from the Credentials tab.
Create Roles
Create the following client roles:
user— basic accessadmin— administrative access
Assign them to test users via Users > Role mappings > Client roles.
For more on role configuration, see our RBAC overview and the post on fine-grained authorization in Keycloak.
Approach 1: nest-keycloak-connect
This is the fastest path to Keycloak integration in NestJS. The nest-keycloak-connect package provides guards, decorators, and a module that handles most of the wiring for you.
Install Dependencies
npm install nest-keycloak-connect keycloak-connect
Your package.json dependencies should include:
{
"dependencies": {
"@nestjs/common": "^10.4.0",
"@nestjs/core": "^10.4.0",
"@nestjs/platform-express": "^10.4.0",
"keycloak-connect": "^26.0.0",
"nest-keycloak-connect": "^1.10.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.0"
}
}
Module Configuration
Register the Keycloak module in your AppModule:
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
KeycloakConnectModule,
ResourceGuard,
RoleGuard,
AuthGuard,
PolicyEnforcementMode,
TokenValidation,
} from 'nest-keycloak-connect';
import { APP_GUARD } from '@nestjs/core';
import { ItemsModule } from './items/items.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
KeycloakConnectModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
authServerUrl: config.get('KEYCLOAK_URL', 'http://localhost:8080'),
realm: config.get('KEYCLOAK_REALM', 'your-realm'),
clientId: config.get('KEYCLOAK_CLIENT_ID', 'nestjs-api'),
secret: config.get('KEYCLOAK_CLIENT_SECRET', ''),
policyEnforcement: PolicyEnforcementMode.PERMISSIVE,
tokenValidation: TokenValidation.ONLINE,
}),
}),
ItemsModule,
UsersModule,
],
providers: [
// Apply guards globally in this order
{ provide: APP_GUARD, useClass: AuthGuard },
{ provide: APP_GUARD, useClass: ResourceGuard },
{ provide: APP_GUARD, useClass: RoleGuard },
],
})
export class AppModule {}
Key configuration options:
PolicyEnforcementMode.PERMISSIVE: Allows access to routes without explicit role requirements. UseENFORCINGif you want all routes protected by default.TokenValidation.ONLINE: Validates tokens against Keycloak on every request (most secure). UseOFFLINEfor local JWT validation (faster).
Create Environment Variables
# .env
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=nestjs-api
KEYCLOAK_CLIENT_SECRET=your-client-secret
Protected Controllers
// items/items.controller.ts
import { Controller, Get, Post, Body, Param, Delete } from '@nestjs/common';
import {
Roles,
Public,
AuthenticatedUser,
Unprotected,
} from 'nest-keycloak-connect';
interface KeycloakUser {
sub: string;
email: string;
preferred_username: string;
realm_access: { roles: string[] };
resource_access: Record<string, { roles: string[] }>;
}
interface Item {
id: number;
name: string;
owner: string;
}
@Controller('items')
export class ItemsController {
private items: Item[] = [
{ id: 1, name: 'Widget', owner: 'user1' },
{ id: 2, name: 'Gadget', owner: 'user2' },
];
@Get()
@Roles({ roles: ['user', 'admin'] })
findAll(@AuthenticatedUser() user: KeycloakUser): Item[] {
return this.items;
}
@Get(':id')
@Roles({ roles: ['user', 'admin'] })
findOne(
@Param('id') id: string,
@AuthenticatedUser() user: KeycloakUser,
): Item | { error: string } {
const item = this.items.find((i) => i.id === parseInt(id));
if (!item) {
return { error: 'Item not found' };
}
return item;
}
@Post()
@Roles({ roles: ['admin'] })
create(
@Body() body: { name: string },
@AuthenticatedUser() user: KeycloakUser,
): Item {
const newItem: Item = {
id: this.items.length + 1,
name: body.name,
owner: user.preferred_username,
};
this.items.push(newItem);
return newItem;
}
@Delete(':id')
@Roles({ roles: ['admin'] })
remove(@Param('id') id: string): { deleted: boolean } {
this.items = this.items.filter((i) => i.id !== parseInt(id));
return { deleted: true };
}
}
// items/items.module.ts
import { Module } from '@nestjs/common';
import { ItemsController } from './items.controller';
@Module({
controllers: [ItemsController],
})
export class ItemsModule {}
The @Roles decorator accepts client roles by default. The @AuthenticatedUser() parameter decorator injects the decoded Keycloak token, giving you access to all JWT claims.
Public Routes
Use the @Public() or @Unprotected() decorator for routes that should be accessible without authentication:
// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { Public } from 'nest-keycloak-connect';
@Controller('health')
export class HealthController {
@Get()
@Public()
check() {
return { status: 'healthy', timestamp: new Date().toISOString() };
}
}
Approach 2: Custom JWT Validation with Passport
For teams that want more control over the authentication flow or need to integrate with multiple identity providers, here’s a custom approach using @nestjs/passport and passport-jwt:
Install Dependencies
npm install @nestjs/passport passport passport-jwt jwks-rsa
npm install -D @types/passport-jwt
JWT Strategy
// auth/keycloak-jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';
export interface KeycloakTokenPayload {
sub: string;
email: string;
preferred_username: string;
realm_access: {
roles: string[];
};
resource_access: Record<
string,
{
roles: string[];
}
>;
scope: string;
iss: string;
aud: string | string[];
}
@Injectable()
export class KeycloakJwtStrategy extends PassportStrategy(
Strategy,
'keycloak',
) {
constructor(private configService: ConfigService) {
const keycloakUrl = configService.get('KEYCLOAK_URL');
const realm = configService.get('KEYCLOAK_REALM');
const issuer = `${keycloakUrl}/realms/${realm}`;
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
issuer: issuer,
algorithms: ['RS256'],
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 10,
jwksUri: `${issuer}/protocol/openid-connect/certs`,
}),
});
}
validate(payload: KeycloakTokenPayload): KeycloakTokenPayload {
// The payload is the decoded JWT. passport-jwt has already
// verified the signature, expiration, and issuer.
return payload;
}
}
The passportJwtSecret function from jwks-rsa automatically fetches and caches Keycloak’s public keys from the JWKS endpoint. The rate limiter prevents excessive key requests.
Auth Module
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { KeycloakJwtStrategy } from './keycloak-jwt.strategy';
@Module({
imports: [PassportModule.register({ defaultStrategy: 'keycloak' })],
providers: [KeycloakJwtStrategy],
exports: [PassportModule],
})
export class AuthModule {}
Custom Guards
// auth/guards/keycloak-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class KeycloakAuthGuard extends AuthGuard('keycloak') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
// auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class KeycloakRolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false;
}
const realmRoles = user.realm_access?.roles || [];
const clientRoles = Object.values(user.resource_access || {}).flatMap(
(resource: any) => resource.roles || [],
);
const allRoles = new Set([...realmRoles, ...clientRoles]);
return requiredRoles.some((role) => allRoles.has(role));
}
}
Custom Decorators
// auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// auth/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
Using the Custom Approach
// app.module.ts (custom approach)
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from './auth/auth.module';
import { KeycloakAuthGuard } from './auth/guards/keycloak-auth.guard';
import { KeycloakRolesGuard } from './auth/guards/roles.guard';
import { ItemsModule } from './items/items.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
AuthModule,
ItemsModule,
],
providers: [
{ provide: APP_GUARD, useClass: KeycloakAuthGuard },
{ provide: APP_GUARD, useClass: KeycloakRolesGuard },
],
})
export class AppModule {}
// items/items.controller.ts (custom approach)
import { Controller, Get, Post, Body, Delete, Param } from '@nestjs/common';
import { Roles } from '../auth/decorators/roles.decorator';
import { Public } from '../auth/decorators/public.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { KeycloakTokenPayload } from '../auth/keycloak-jwt.strategy';
@Controller('items')
export class ItemsController {
@Get()
@Roles('user', 'admin')
findAll(@CurrentUser() user: KeycloakTokenPayload) {
return {
items: [
{ id: 1, name: 'Widget', owner: user.preferred_username },
{ id: 2, name: 'Gadget', owner: user.preferred_username },
],
};
}
@Post()
@Roles('admin')
create(
@Body() body: { name: string },
@CurrentUser('preferred_username') username: string,
) {
return { id: 3, name: body.name, owner: username };
}
@Delete(':id')
@Roles('admin')
remove(@Param('id') id: string) {
return { deleted: true, id: parseInt(id) };
}
}
Which Approach Should You Choose?
| Criteria | nest-keycloak-connect | Custom Passport |
|---|---|---|
| Setup time | Faster | More initial work |
| Customization | Limited to package API | Full control |
| Multiple IdPs | Keycloak only | Any OIDC provider |
| Maintenance | Depends on package updates | You own it |
| Online validation | Built-in | Requires custom code |
| Learning curve | Lower | Moderate |
Use nest-keycloak-connect if Keycloak is your only identity provider and you want rapid setup. Use the custom Passport approach if you need flexibility, plan to support multiple providers, or want to avoid a dependency on a third-party NestJS package.
GraphQL Integration
If you’re using @nestjs/graphql, both approaches work with GraphQL resolvers. Here’s an example using the custom decorators:
// items/items.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { KeycloakTokenPayload } from '../auth/keycloak-jwt.strategy';
@Resolver('Item')
export class ItemsResolver {
@Query('items')
@Roles('user', 'admin')
findAll(@CurrentUser() user: KeycloakTokenPayload) {
return [
{ id: 1, name: 'Widget', owner: user.preferred_username },
{ id: 2, name: 'Gadget', owner: user.preferred_username },
];
}
@Mutation('createItem')
@Roles('admin')
create(
@Args('name') name: string,
@CurrentUser() user: KeycloakTokenPayload,
) {
return { id: 3, name, owner: user.preferred_username };
}
}
For GraphQL, the guards and decorators work identically to REST controllers because NestJS uses the same execution context under the hood. The @CurrentUser() decorator extracts the user from the request regardless of the transport layer.
Run and Test
npm run start:dev
Get a Token
TOKEN=$(curl -s -X POST
http://localhost:8080/realms/your-realm/protocol/openid-connect/token
-H "Content-Type: application/x-www-form-urlencoded"
-d "client_id=nestjs-api"
-d "client_secret=your-client-secret"
-d "grant_type=password"
-d "username=testuser"
-d "password=testpassword"
| jq -r '.access_token')
Test Endpoints
# Health check (public)
curl http://localhost:3000/health
# List items (requires 'user' or 'admin' role)
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/items
# Create item (requires 'admin' role)
curl -X POST -H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
-d '{"name": "New Item"}'
http://localhost:3000/items
Use our JWT Token Analyzer to decode the access token and verify that realm and client roles are present. If roles are missing, check the client scope mappers in Keycloak — make sure “Add to access token” is enabled for the roles mapper.
Production Security Checklist
Before deploying to production, address these items:
-
Use HTTPS everywhere. Configure TLS between your NestJS app, Keycloak, and all clients. If running Keycloak behind a reverse proxy, see our guide on running Keycloak behind a reverse proxy.
-
Set short access token lifetimes. Configure 5-15 minute access tokens in Keycloak. Use refresh tokens for longer sessions.
-
Enable audit logging. Track authentication events in Keycloak for compliance and security monitoring. Our guide on auditing in Keycloak explains how.
-
Enforce MFA for admin users. Keycloak supports conditional MFA based on roles or application context.
-
Validate the audience claim. Ensure your JWT validation checks the
audclaim to prevent tokens issued for other clients from being accepted. -
Monitor session activity. Use Keycloak’s session management to track and terminate active sessions when needed.
-
Rate-limit authentication endpoints. Protect against brute force attacks on your token-issuing endpoints.
What’s Next
With your NestJS API secured by Keycloak, consider these extensions:
- Implement SCIM provisioning for automated user management. Test your endpoints with the SCIM Endpoint Tester.
- Add identity brokering for social login or enterprise SSO connections.
- Build a frontend with Next.js — see our Next.js + Keycloak guide.
- Integrate a Python backend — see our FastAPI + Keycloak guide or Django REST Framework + Keycloak guide.
- Explore the Keycloak securing applications guide for additional integration options.
- Review Keycloak token validation patterns for advanced scenarios.
Let us handle Keycloak for you. Skycloak provides fully managed Keycloak with automated scaling, security patches, and 99.99% uptime SLA. See pricing or explore our security practices.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.