A bearer-only Keycloak authentication and authorization module for NestJS. Uses standard OIDC discovery and has zero runtime dependency on keycloak-connect.
- Bearer-token API authentication and authorization for NestJS.
- OIDC discovery — endpoints resolved from
.well-known/openid-configuration(with fallback). - ONLINE and OFFLINE token validation (introspection + JWKS signature verification).
- Algorithm allowlist — only RS, ES, and PS family algorithms are accepted during offline validation.
- Per-realm
notBeforerevocation state for multi-tenant safety. - Resource/scope authorization via UMA (
@Resource,@Scopes,@ConditionalScopes). - Role authorization (
@Roles) with configurable role merge and match modes. - OIDC back-channel logout (
sid/subrevocation). - Typed error hierarchy — all library errors extend
KeycloakAuthErrorfor easy catching. - Compatible with Fastify platform.
- This package is designed for bearer-only API/server flows.
- It does not implement browser/session middleware flows such as login redirects, auth-code callback exchange, session/cookie grant stores, or logout endpoints.
- It implements Keycloak admin callback endpoints:
POST /k_push_not_beforefor realmnotBeforerevocation updates (used by OFFLINE token validation).POST /k_logoutfor OIDC back-channel logout token handling (sid/subrevocation).
yarn add nestjs-keycloak-authnpm install nestjs-keycloak-auth --saveRegistering the module:
KeycloakAuthModule.register({
authServerUrl: 'http://localhost:8080', // might be http://localhost:8080/auth for older keycloak versions
realm: 'master',
clientId: 'my-nestjs-app',
secret: 'secret',
bearerOnly: true,
policyEnforcement: PolicyEnforcementMode.PERMISSIVE, // optional
tokenValidation: TokenValidation.ONLINE, // optional
backchannelLogoutTtlMs: 24 * 60 * 60 * 1000, // optional, defaults to 24 hours
clockSkewSec: 5, // optional, defaults to 0; used by OFFLINE expiration checks
});Async registration is also available:
KeycloakAuthModule.registerAsync({
useExisting: KeycloakConfigService,
imports: [ConfigModule],
});import { Injectable } from '@nestjs/common';
import {
KeycloakAuthOptions,
KeycloakAuthOptionsFactory,
PolicyEnforcementMode,
TokenValidation,
} from 'nestjs-keycloak-auth';
@Injectable()
export class KeycloakConfigService implements KeycloakAuthOptionsFactory {
createKeycloakAuthOptions(): KeycloakAuthOptions {
return {
// http://localhost:8080/auth for older keycloak versions
authServerUrl: 'http://localhost:8080',
realm: 'master',
clientId: 'my-nestjs-app',
secret: 'secret',
bearerOnly: true,
policyEnforcement: PolicyEnforcementMode.PERMISSIVE,
tokenValidation: TokenValidation.ONLINE,
backchannelLogoutTtlMs: 24 * 60 * 60 * 1000,
clockSkewSec: 5,
};
}
}You can also register by just providing the keycloak.json path and an optional module configuration:
KeycloakAuthModule.register(`./keycloak.json`, {
policyEnforcement: PolicyEnforcementMode.PERMISSIVE,
tokenValidation: TokenValidation.ONLINE,
});Register any of the guards either globally, or scoped in your controller.
NOTE: These are in order, see https://docs.nestjs.com/guards#binding-guards for more information.
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
{
provide: APP_GUARD,
useClass: ResourceGuard,
},
{
provide: APP_GUARD,
useClass: RoleGuard,
},
];@Controller('cats')
@UseGuards(AuthGuard, ResourceGuard)
export class CatsController {}Adds an authentication guard, you can also have it scoped if you like (using regular @UseGuards(AuthGuard) in your controllers). By default, it will throw a 401 unauthorized when it is unable to verify the JWT token or Bearer header is missing.
Adds a resource guard, which is permissive by default (can be configured see options). Only controllers annotated with @Resource and methods with @Scopes are handled by this guard.
NOTE: This guard is not necessary if you are using role-based authorization exclusively. You can use role guard exclusively for that.
Adds a role guard, can only be used in conjunction with resource guard when enforcement policy is PERMISSIVE, unless you only use role guard exclusively.
Permissive by default. Used by controller methods annotated with @Roles (matching can be configured)
In your controllers, simply do:
import {
Resource,
Roles,
Scopes,
Public,
RoleMatchingMode,
} from 'nestjs-keycloak-auth';
import { Controller, Get, Delete, Put, Post, Param } from '@nestjs/common';
import { Product } from './product';
import { ProductService } from './product.service';
@Controller()
@Resource(Product.name)
export class ProductController {
constructor(private service: ProductService) {}
@Get()
@Public()
async findAll() {
return await this.service.findAll();
}
@Get()
@Roles({ roles: ['admin', 'other'] })
async findAllBarcodes() {
return await this.service.findAllBarcodes();
}
@Get(':code')
@Scopes('View')
async findByCode(@Param('code') code: string) {
return await this.service.findByCode(code);
}
@Post()
@Scopes('Create')
@ConditionalScopes((request, token) => {
if (token.hasRealmRole('sysadmin')) {
return ['Overwrite'];
}
return [];
})
async create(@Body() product: Product) {
return await this.service.create(product);
}
@Delete(':code')
@Scopes('Delete')
@Roles({ roles: ['admin', 'realm:sysadmin'], mode: RoleMatchingMode.ALL })
async deleteByCode(@Param('code') code: string) {
return await this.service.deleteByCode(code);
}
@Put(':code')
@Scopes('Edit')
async update(@Param('code') code: string, @Body() product: Product) {
return await this.service.update(code, product);
}
}Here are the decorators you can use in your controllers.
| Decorator | Description |
|---|---|
| @AuthenticatedUser | Retrieves the current Keycloak logged-in user. (must be per method, unless controller is request scoped.) |
| @AccessToken | Retrieves the access token used in the request |
| @ResolvedScopes | Retrieves the resolved scopes (used in @ConditionalScopes) |
| @EnforcerOptions | Keycloak enforcer options. |
| @Public | Allow any user to use the route. |
| @Resource | Keycloak application resource name. |
| @Scopes | Keycloak application scopes. |
| @ConditionalScopes | Conditional keycloak application scopes. |
| @Roles | Keycloak realm/application roles. |
| @TokenScopes | Required OAuth scopes on the access token. |
Setting up for multi-tenant is configured as an option in your configuration:
{
// Add /auth for older keycloak versions
authServerUrl: 'http://localhost:8180/', // will be used as fallback
clientId: 'nest-api', // will be used as fallback
secret: 'fallback', // will be used as fallback
multiTenant: {
resolveAlways: true,
realmResolver: (request) => {
return request.get('host').split('.')[0];
},
realmSecretResolver: (realm, request) => {
const secrets = { master: 'secret', slave: 'password' };
return secrets[realm];
},
realmClientIdResolver: (realm, request) => {
const clientIds = { master: 'angular-app', slave: 'vue-app' };
return clientIds[realm];
},
// note to add /auth for older keycloak versions
realmAuthServerUrlResolver: (realm, request) => {
const authServerUrls = { master: 'https://master.local/', slave: 'https://slave.local/' };
return authServerUrls[realm];
}
}
}This module mounts Keycloak admin callback endpoints:
POST /k_push_not_beforePOST /k_logout
Purpose:
- Accepts signed Keycloak admin callbacks with action
PUSH_NOT_BEFORE. - Updates token revocation cutoff (
notBefore) used by OFFLINE validation. - Stores
notBeforeper realm URL, so one realm update does not affect another realm in multi-tenant setups. - Accepts signed OIDC back-channel logout tokens and revokes token usage by
sidand/orsub.
Realm resolution for callback verification:
multiTenant.realmResolver(request)when configured- Single-tenant configured realm (
realm)
k_logout here is callback-only revocation handling for bearer tokens. It does not add browser/session middleware flows.
All errors thrown by the library extend KeycloakAuthError, so you can catch any library error with a single instanceof check:
import {
KeycloakAuthError,
KeycloakConfigError,
KeycloakTokenError,
KeycloakPermissionError,
KeycloakAdminError,
} from 'nestjs-keycloak-auth';| Error class | Code | When |
|---|---|---|
KeycloakAuthError |
KEYCLOAK_AUTH_ERROR |
Base class — catches all library errors via instanceof |
KeycloakConfigError |
KEYCLOAK_CONFIG_ERROR |
Missing config, invalid options, file not found |
KeycloakTokenError |
KEYCLOAK_TOKEN_ERROR |
Malformed JWT, grant validation failure, JWKS key miss |
KeycloakPermissionError |
KEYCLOAK_PERMISSION_ERROR |
UMA permission check failure |
KeycloakAdminError |
KEYCLOAK_ADMIN_ERROR |
Admin callback signature/token verification failure |
Guards continue to throw standard NestJS UnauthorizedException / ForbiddenException at the HTTP boundary.
The example/ folder contains a complete working NestJS application with:
- Docker Compose setup (Keycloak + PostgreSQL + NestJS API)
- Pre-configured Keycloak realm exports (single tenant + two tenants)
- Postman collection for testing all endpoints
- Product CRUD controller demonstrating
@Resource,@Scopes,@ConditionalScopes,@Roles,@EnforcerOptions, and UMA response modes
See example/README.md for setup and usage instructions.
npm test
npm run test:covCurrent test setup uses Jest + ts-jest and is configured to enforce 100% global coverage thresholds.
| Option | Description | Required | Default |
|---|---|---|---|
| policyEnforcement | Sets the policy enforcement mode | no | PERMISSIVE |
| tokenValidation | Sets the token validation method | no | ONLINE |
| multiTenant | Sets options for multi-tenant configuration | no | - |
| roleMerge | Sets the merge mode for @Roles decorator |
no | OVERRIDE |
| backchannelLogoutTtlMs | TTL for in-memory back-channel logout revocation entries in milliseconds | no | 86400000 |
| clockSkewSec | Allowed clock skew for OFFLINE token expiration checks in seconds | no | 0 |
| Option | Description |
|---|---|
realm |
Realm name |
clientId / client-id |
Client ID (or resource) |
secret / credentials.secret |
Client secret (confidential clients) |
authServerUrl / serverUrl |
Keycloak base URL |
bearerOnly / bearer-only |
Marks bearer-only behavior |
public / public-client |
Public client mode |
realmPublicKey |
Static realm public key for OFFLINE validation |
verifyTokenAudience |
Enables strict audience check in OFFLINE validation |
minTimeBetweenJwksRequests |
JWKS retry throttle |
| Option | Description | Required | Default |
|---|---|---|---|
| resolveAlways | Always resolve realm config instead of using cached values | no | false |
| realmResolver | Resolves realm from request | yes | - |
| realmSecretResolver | Resolves secret by realm (and optional request) | no | - |
| realmAuthServerUrlResolver | Resolves auth server URL by realm (and optional request) | no | - |
| realmClientIdResolver | Resolves client ID by realm (and optional request) | yes | - |
See CONTRIBUTING.md for development setup and guidelines.
To report a vulnerability, see SECURITY.md. Do not open a public issue.
Inspired by nest-keycloak-connect by John Joshua Ferrer and the official keycloak-nodejs-connect adapter.