npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pawells/nestjs-auth

v1.1.2

Published

NestJS Keycloak integration library — token validation, admin API, and federated identity

Readme

NestJS Authentication Module

GitHub Release CI npm version Node License: MIT GitHub Sponsors

Keycloak integration library for NestJS resource servers. Validates Keycloak-issued access tokens (online introspection by default; offline JWKS opt-in), enforces role and permission guards on HTTP and GraphQL routes, and provides a typed Admin REST API client for user management, federated identity, and event polling.

This package does not issue tokens, manage passwords, or run login flows — those are Keycloak's responsibility.

Table of Contents

Installation

yarn add @pawells/nestjs-auth

Peer Dependencies

| Package | Version | Required | |---|---|---| | @nestjs/common | >=10.0.0 | Yes | | @nestjs/core | >=10.0.0 | Yes | | @nestjs/jwt | >=10.0.0 | Yes | | @nestjs/terminus | >=10.0.0 | Yes | | joi | >=17.0.0 | Yes | | @nestjs/graphql | >=12.0.0 | Yes — required to use GraphQL decorators | | jwks-rsa | >=3.0.0 | No — required only for offline (JWKS) validation mode |

Quick Start

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
  KeycloakModule,
  KeycloakAdminModule,
  JwtAuthGuard,
} from '@pawells/nestjs-auth';

@Module({
  imports: [
    KeycloakModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        authServerUrl: config.get('KEYCLOAK_AUTH_SERVER_URL'),
        realm: config.get('KEYCLOAK_REALM'),
        clientId: config.get('KEYCLOAK_CLIENT_ID'),
        clientSecret: config.get('KEYCLOAK_CLIENT_SECRET'),
      }),
    }),
    KeycloakAdminModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        enabled: config.get('KEYCLOAK_ADMIN_ENABLED') === 'true',
        baseUrl: config.get('KEYCLOAK_BASE_URL'),
        realmName: config.get('KEYCLOAK_REALM'),
        credentials: {
          type: 'clientCredentials',
          clientId: config.get('KEYCLOAK_ADMIN_CLIENT_ID'),
          clientSecret: config.get('KEYCLOAK_ADMIN_CLIENT_SECRET'),
        },
      }),
    }),
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AppModule {}

KeycloakModule

KeycloakModule configures token validation for the service. It provides KeycloakTokenValidationService to all modules via its exports.

Options

| Field | Type | Default | Description | |---|---|---|---| | authServerUrl | string | — | Keycloak realm base URL, e.g. https://auth.example.com/realms/myrealm | | realm | string | — | Keycloak realm name | | clientId | string | — | This service's Keycloak client ID — used for audience validation and client role extraction | | validationMode | 'online' \| 'offline' | 'online' | Token validation strategy — see Token Validation Modes | | clientSecret | string | — | Client secret for the introspection endpoint. Required when validationMode is 'online' (the default) | | jwksCacheTtlMs | number | 300000 | JWKS public key cache TTL in milliseconds. Used in offline mode only | | issuer | string | authServerUrl | Expected iss claim value. Must match exactly. Defaults to authServerUrl |

forRoot

KeycloakModule.forRoot({
  authServerUrl: 'https://auth.example.com/realms/myrealm',
  realm: 'myrealm',
  clientId: 'my-service',
  clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
});

forRootAsync

KeycloakModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    authServerUrl: config.get('KEYCLOAK_AUTH_SERVER_URL'),
    realm: config.get('KEYCLOAK_REALM'),
    clientId: config.get('KEYCLOAK_CLIENT_ID'),
    clientSecret: config.get('KEYCLOAK_CLIENT_SECRET'),
  }),
});

Token Validation Modes

Online (default)

Validates each token by calling Keycloak's introspection endpoint (/protocol/openid-connect/token/introspect). The introspection response is authoritative: it detects revoked tokens and expired sessions immediately.

  • Requires clientSecret
  • Adds a network round-trip per request
  • Recommended for most deployments

Offline (opt-in)

Validates the JWT signature locally using Keycloak's JWKS endpoint. Public keys are fetched once and cached.

  • Does not detect revocation — a revoked token remains valid until its exp claim passes
  • No network hop after the initial key fetch
  • Validates exp, iss, and aud claims locally
  • Requires the jwks-rsa peer dependency
  • Set validationMode: 'offline' to enable
KeycloakModule.forRoot({
  authServerUrl: 'https://auth.example.com/realms/myrealm',
  realm: 'myrealm',
  clientId: 'my-service',
  validationMode: 'offline',
  jwksCacheTtlMs: 600000, // 10 minutes
});

Use offline mode only when request throughput makes per-request introspection impractical and token lifetimes are short enough to bound the revocation window.

Guards

JwtAuthGuard

Validates the Keycloak access token on every incoming request. Extracts the Bearer token from the Authorization header, calls KeycloakTokenValidationService.validateToken, and attaches the resolved KeycloakUser to request.user.

Routes decorated with @Public() bypass the guard entirely.

Register globally (recommended):

import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from '@pawells/nestjs-auth';

// In your AppModule providers array:
{
  provide: APP_GUARD,
  useClass: JwtAuthGuard,
}

Per-route:

import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '@pawells/nestjs-auth';

@UseGuards(JwtAuthGuard)
@Controller('profile')
export class ProfileController {}

RoleGuard

Checks whether the authenticated user holds at least one of the roles listed in @Roles(). Roles are matched against the union of realm_access.roles and resource_access[clientId].roles from the token.

import { UseGuards } from '@nestjs/common';
import { RoleGuard, Roles } from '@pawells/nestjs-auth';

@UseGuards(RoleGuard)
@Controller('admin')
export class AdminController {
  @Roles('admin', 'moderator')
  @Get('users')
  listUsers() {
    return [];
  }
}

PermissionGuard

Checks whether the authenticated user holds at least one of the values listed in @Permissions(), resolved against the same role arrays as RoleGuard.

import { UseGuards } from '@nestjs/common';
import { PermissionGuard, Permissions } from '@pawells/nestjs-auth';

@UseGuards(PermissionGuard)
@Controller('documents')
export class DocumentsController {
  @Permissions('document.write')
  @Post()
  createDocument(@Body() dto: CreateDocumentDto) {
    return {};
  }
}

Decorators

HTTP Decorators

| Decorator | Type | Description | |---|---|---| | @Auth() | Method | Marks the route as requiring authentication (sets isPublic: false) | | @Public() | Method | Marks the route as public — JwtAuthGuard skips validation | | @Roles(...roles) | Method | Specifies role requirements for RoleGuard | | @Permissions(...permissions) | Method | Specifies permission requirements for PermissionGuard | | @CurrentUser(property?) | Parameter | Injects the KeycloakUser from request.user, or a specific property if property is given | | @AuthToken() | Parameter | Injects the raw Bearer token string from the Authorization header |

import { Controller, Get } from '@nestjs/common';
import { Auth, Public, Roles, CurrentUser, AuthToken } from '@pawells/nestjs-auth';
import type { KeycloakUser } from '@pawells/nestjs-auth';

@Controller('me')
export class ProfileController {
  @Public()
  @Get('ping')
  ping() {
    return 'pong';
  }

  @Auth()
  @Get()
  getProfile(@CurrentUser() user: KeycloakUser) {
    return user;
  }

  @Roles('admin')
  @Get('token')
  getToken(@AuthToken() token: string) {
    return { token };
  }

  @Get('id')
  getId(@CurrentUser('id') userId: string) {
    return { userId };
  }
}

GraphQL Decorators

The GraphQL variants are aliases of the HTTP decorators, pre-configured for the GraphQL execution context.

| Decorator | Equivalent to | Notes | |---|---|---| | @GraphQLAuth() | @Auth() | Marks GraphQL resolver as requiring authentication | | @GraphQLPublic() | @Public() | Marks GraphQL resolver as public | | @GraphQLRoles(...roles) | @Roles(...roles) | Specifies role requirements | | @GraphQLCurrentUser(property?) | @CurrentUser(property?, { contextType: 'graphql' }) | Injects user from GraphQL context | | @GraphQLUser(property?) | @GraphQLCurrentUser(property?) | Alias | | @GraphQLAuthToken() | @AuthToken({ contextType: 'graphql' }) | Injects Bearer token from GraphQL context | | @GraphQLContextParam() | — | Injects the full GraphQL context object |

import { Resolver, Query, Mutation } from '@nestjs/graphql';
import {
  GraphQLAuth,
  GraphQLPublic,
  GraphQLRoles,
  GraphQLCurrentUser,
  GraphQLAuthToken,
} from '@pawells/nestjs-auth';
import type { KeycloakUser } from '@pawells/nestjs-auth';

@Resolver()
export class UserResolver {
  @GraphQLPublic()
  @Query(() => String)
  async health(): Promise<string> {
    return 'ok';
  }

  @GraphQLAuth()
  @Query(() => String)
  async me(@GraphQLCurrentUser() user: KeycloakUser): Promise<string> {
    return user.id;
  }

  @GraphQLRoles('admin')
  @Query(() => [String])
  async listUsers(): Promise<string[]> {
    return [];
  }

  @Mutation(() => Boolean)
  async validateToken(@GraphQLAuthToken() token: string): Promise<boolean> {
    return !!token;
  }
}

KeycloakAdminModule

KeycloakAdminModule provides a typed client for the Keycloak Admin REST API. It is registered as a global module — import it once in AppModule and inject KeycloakAdminService anywhere.

Options (KeycloakAdminConfig)

| Field | Type | Default | Description | |---|---|---|---| | enabled | boolean | false | When false the client is not initialized — useful for disabling in test environments | | baseUrl | string | 'http://localhost:8080' | Keycloak server base URL (not realm-specific) | | realmName | string | 'master' | Target realm for all Admin API calls | | credentials.type | 'password' \| 'clientCredentials' | 'password' | Authentication method | | credentials.username | string | — | Admin username (password auth only) | | credentials.password | string | — | Admin password (password auth only) | | credentials.clientId | string | — | Service account client ID (clientCredentials auth only) | | credentials.clientSecret | string | — | Service account client secret (clientCredentials auth only) | | timeout | number | 30000 | Request timeout in milliseconds | | retry.maxRetries | number | 3 | Maximum retry attempts on transient failures | | retry.retryDelay | number | 1000 | Delay between retries in milliseconds |

forRoot

KeycloakAdminModule.forRoot({
  enabled: process.env.KEYCLOAK_ADMIN_ENABLED === 'true',
  baseUrl: 'https://auth.example.com',
  realmName: 'myrealm',
  credentials: {
    type: 'clientCredentials',
    clientId: process.env.KEYCLOAK_ADMIN_CLIENT_ID,
    clientSecret: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET,
  },
});

forRootAsync

KeycloakAdminModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    enabled: config.get('KEYCLOAK_ADMIN_ENABLED') === 'true',
    baseUrl: config.get('KEYCLOAK_BASE_URL'),
    realmName: config.get('KEYCLOAK_REALM'),
    credentials: {
      type: 'clientCredentials',
      clientId: config.get('KEYCLOAK_ADMIN_CLIENT_ID'),
      clientSecret: config.get('KEYCLOAK_ADMIN_CLIENT_SECRET'),
    },
  }),
});

KeycloakAdminService

Inject KeycloakAdminService and access the sub-services via its properties.

| Property | Service | Responsibility | |---|---|---| | .users | UserService | Create, read, update, delete users; assign roles and groups | | .roles | RoleService | Manage realm and client roles | | .realms | RealmService | Realm-level configuration and queries | | .clients | ClientService | Manage clients and client scopes | | .groups | GroupService | Create and manage groups; add/remove members | | .identityProviders | IdentityProviderService | Manage identity provider configurations | | .authentication | AuthenticationService | Manage authentication flows | | .federatedIdentity | FederatedIdentityService | Link and unlink external provider identities — see Federated Identity | | .events | EventService | Query admin and access events — see Event Polling |

import { Injectable } from '@nestjs/common';
import { KeycloakAdminService } from '@pawells/nestjs-auth';

@Injectable()
export class UserManagementService {
  constructor(private readonly keycloak: KeycloakAdminService) {}

  async createUser(email: string, firstName: string): Promise<void> {
    await this.keycloak.users.create({
      email,
      firstName,
      enabled: true,
    });
  }

  async assignRole(userId: string, roleName: string): Promise<void> {
    await this.keycloak.users.assignRole(userId, roleName);
  }
}

Call keycloakAdminService.isEnabled() before calling sub-services if the module may be disabled in the current environment.

Federated Identity

KeycloakAdminService.federatedIdentity manages links between Keycloak user accounts and external identity providers.

| Method | Signature | Description | |---|---|---| | list | (userId: string) => Promise<FederatedIdentityLink[]> | Returns all provider links for a user | | link | (userId: string, provider: string, link: { userId: string; userName: string }) => Promise<void> | Links an external provider identity to a Keycloak user | | unlink | (userId: string, provider: string) => Promise<void> | Removes a provider link from a Keycloak user |

link performs a pre-flight list check and throws ConflictError if a link for the same provider and external user ID already exists. This is a workaround for Keycloak issue #34608, which can create duplicate federated identity records.

import { Injectable } from '@nestjs/common';
import { KeycloakAdminService, ConflictError } from '@pawells/nestjs-auth';

@Injectable()
export class IdentityLinkingService {
  constructor(private readonly keycloak: KeycloakAdminService) {}

  async linkGoogleAccount(
    keycloakUserId: string,
    googleUserId: string,
    googleEmail: string,
  ): Promise<void> {
    try {
      await this.keycloak.federatedIdentity.link(keycloakUserId, 'google', {
        userId: googleUserId,
        userName: googleEmail,
      });
    } catch (error) {
      if (error instanceof ConflictError) {
        // Already linked — treat as a no-op or surface to the caller
        return;
      }
      throw error;
    }
  }

  async listLinks(keycloakUserId: string) {
    return this.keycloak.federatedIdentity.list(keycloakUserId);
  }
}

Event Polling

KeycloakAdminService.events queries Keycloak's event log for both admin (resource mutation) and access (login, logout, token) events.

Methods

| Method | Signature | Description | |---|---|---| | getAdminEvents | (query?: AdminEventQuery) => Promise<KeycloakAdminEvent[]> | Returns admin events matching the query | | getAccessEvents | (query?: AccessEventQuery) => Promise<KeycloakAccessEvent[]> | Returns access events matching the query |

AdminEventQuery Fields

| Field | Type | Description | |---|---|---| | operationTypes | ('CREATE' \| 'UPDATE' \| 'DELETE' \| 'ACTION')[] | Filter by operation type | | resourceTypes | string[] | Filter by resource type (e.g. ['USER']) | | resourcePath | string | Filter by resource path prefix | | dateFrom | Date | Earliest event timestamp (inclusive) | | dateTo | Date | Latest event timestamp (inclusive) | | first | number | Pagination offset | | max | number | Maximum results to return |

AccessEventQuery supports the same date and pagination fields plus type (string array), client, and user.

KeycloakAdminEvent Fields

| Field | Type | Notes | |---|---|---| | time | number | Unix timestamp in milliseconds | | realmId | string | Realm identifier | | operationType | 'CREATE' \| 'UPDATE' \| 'DELETE' \| 'ACTION' | Type of operation | | resourceType | string | Resource category, e.g. USER, GROUP | | resourcePath | string | Path to the affected resource | | representation | string \| undefined | JSON-encoded resource snapshot. Present on CREATE and UPDATE only. Must be parsed with JSON.parse() before use | | authDetails | object \| undefined | Actor details: realmId, clientId, userId, ipAddress |

Checkpoint Cursor Pattern

Keycloak does not provide a persistent event cursor. To avoid re-processing events or missing events between polls, track the time of the most recently processed event and pass it as dateFrom on subsequent polls. Use a page size (max) that fits within your Keycloak event retention window — events older than the retention period are purged and will be lost if polling falls behind.

import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { KeycloakAdminService } from '@pawells/nestjs-auth';

@Injectable()
export class EventSyncService {
  private lastProcessedTime: Date = new Date(Date.now() - 60_000);

  constructor(private readonly keycloak: KeycloakAdminService) {}

  @Cron('*/30 * * * * *') // every 30 seconds
  async pollAdminEvents(): Promise<void> {
    const events = await this.keycloak.events.getAdminEvents({
      dateFrom: this.lastProcessedTime,
      operationTypes: ['CREATE', 'UPDATE', 'DELETE'],
      resourceTypes: ['USER'],
      max: 100,
    });

    for (const event of events) {
      await this.processEvent(event);
      const eventTime = new Date(event.time);
      if (eventTime > this.lastProcessedTime) {
        this.lastProcessedTime = eventTime;
      }
    }
  }

  private async processEvent(event: any): Promise<void> {
    // Handle the event
  }
}

Keycloak Client Configuration

Three Keycloak clients are typically required when using this package.

React SPA (public client)

  • Client type: Public
  • Authentication flow: Standard flow enabled
  • PKCE: Required — set Code Challenge Method to S256
  • Valid redirect URIs: Your frontend origin(s)

NestJS resource server (confidential client)

This is the client whose clientId and clientSecret you provide to KeycloakModule. It is not used to authenticate users — it authenticates the service itself for introspection calls.

  • Client type: Confidential
  • Service accounts: Not required
  • Client authentication: Enabled
  • Introspection requires a client secret — keep validationMode: 'online' unless you have a specific reason to use offline mode

If you are using offline (JWKS) validation exclusively, a confidential client is not required for token validation. The JWKS endpoint is public.

Admin API caller (confidential service account)

This is the client whose credentials you provide to KeycloakAdminModule.

  • Client type: Confidential
  • Service accounts: Enabled
  • Required service account roles (assigned in the realm-management client):
    • manage-users — create, update, delete users and assign roles
    • manage-identity-providers — link and unlink federated identities
    • view-events — read admin and access events

Security Notes

Online introspection is the recommended validation mode. It is authoritative: a revoked Keycloak session is rejected immediately, regardless of token expiry.

Offline JWKS validation does not detect revocation. A token that has been revoked in Keycloak (e.g. by logging out or disabling the user) continues to pass offline validation until its exp claim expires. Only use offline mode when throughput requirements make per-request introspection impractical, and mitigate the revocation window by setting short token lifetimes in Keycloak (5 minutes or less).

Federated identity deduplication (Keycloak #34608). Keycloak's Admin API can create duplicate federated identity records if addToFederatedIdentity is called concurrently for the same user and provider. The FederatedIdentityService.link method guards against this with a pre-flight check, but the check is not atomic. Under high concurrency, implement external coordination (e.g. a distributed lock) if duplicate links are not tolerable.

Event polling and retention windows. Keycloak purges events based on a configurable retention period. If your poll interval exceeds the retention window — or if polling stops and then resumes — events will be permanently lost. Poll at a frequency significantly shorter than the retention window, and align the retention window with your operational requirements in the Keycloak realm settings (Admin Console > Realm Settings > Events).

Related Packages

License

MIT