# @pawells/nestjs-auth

[![GitHub Release](https://img.shields.io/github/v/release/PhillipAWells/workspace?filter=nestjs-auth%40*&label=release)](https://github.com/PhillipAWells/workspace/releases?q=nestjs-auth)
[![CI](https://github.com/PhillipAWells/workspace/actions/workflows/ci.yml/badge.svg)](https://github.com/PhillipAWells/workspace/actions/workflows/ci.yml)
[![npm version](https://img.shields.io/npm/v/@pawells/nestjs-auth.svg)](https://www.npmjs.com/package/@pawells/nestjs-auth)
[![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/PhillipAWells)](https://github.com/sponsors/PhillipAWells)

## Description

Decorator-based guards and Keycloak bridge for NestJS resource servers. Provides JWT token validation (online introspection or offline JWKS), role and permission authorization, and parameter decorators for injecting the authenticated user and raw token claims.

## Requirements

- **Node.js** `>=22.0.0`

### Peer Dependencies

| Package | Version |
|---|---|
| `@nestjs/common` | `^11.1.28` |
| `@nestjs/core` | `^11.1.28` |
| `@nestjs/graphql` | `^13.4.0` |

### Runtime Dependencies

`reflect-metadata` and `rxjs` are required by **NestJS itself** at runtime (for decorator metadata
and its internal reactive APIs), not by this package directly — `@pawells/nestjs-auth` doesn't
import either of them. Any application depending on `@nestjs/common`/`@nestjs/core` already needs
both installed; they're listed in the install command below for completeness, matching the
`reflect-metadata` bootstrap requirement described under Installation.

### External Service Requirements

- **Keycloak** — a reachable Keycloak server instance (realm, client ID, and client secret required for online mode)

## Installation

```sh
yarn add @pawells/nestjs-auth @nestjs/common @nestjs/core @nestjs/graphql reflect-metadata rxjs
```

NestJS requires `emitDecoratorMetadata` and `experimentalDecorators` to be enabled in your
application's `tsconfig.json`. Ensure `import 'reflect-metadata'` appears at the top of your
application entry point before any NestJS bootstrapping.

## Quick Start

Register `KeycloakModule` at the application root and mount `JwtAuthGuard` globally. All routes require a valid JWT by default; annotate public endpoints with `@Public()` to opt out.

```ts
import 'reflect-metadata';
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { KeycloakModule, JwtAuthGuard } from '@pawells/nestjs-auth';

@Module({
	imports: [
		KeycloakModule.forRoot({
			authServerUrl: process.env.KEYCLOAK_AUTH_SERVER_URL || 'https://auth.example.com',
			realm: process.env.KEYCLOAK_REALM || 'myrealm',
			clientId: process.env.KEYCLOAK_CLIENT_ID || 'my-client',
			clientSecret: process.env.KEYCLOAK_CLIENT_SECRET || 'secret',
			validationMode: 'online',
		}),
	],
	providers: [
		{ provide: APP_GUARD, useClass: JwtAuthGuard },
	],
})
export class AppModule {}
```

Apply role and permission guards per-controller and use parameter decorators to inject the authenticated user or raw token:

```ts
	import { Controller, Get } from '@nestjs/common';
	import { UseGuards } from '@nestjs/common';
	import {
		JwtAuthGuard,
		RoleGuard,
		PermissionGuard,
		Public,
		Roles,
		Permissions,
		RequireAllRoles,
		RequireAllPermissions,
		CurrentUser,
		AuthToken,
		KeycloakClaims,
	} from '@pawells/nestjs-auth';
	import type { IKeycloakUser, IKeycloakTokenClaims } from '@pawells/nestjs-auth';

	@UseGuards(JwtAuthGuard, RoleGuard, PermissionGuard)
	@Controller('api')
	export class ExampleController {
		@Public()
		@Get('health')
		checkHealth() {
			return { status: 'ok' };
		}

		@Get('profile')
		getProfile(@CurrentUser() user: IKeycloakUser) {
			return user;
		}

		@Roles('admin', 'moderator')
		@Get('admin/users')
		listUsers(@CurrentUser('id') userId: string) {
			return { requestedBy: userId };
		}

		@RequireAllRoles('admin', 'auditor')
		@Get('audit/logs')
		auditLogs() {
			// User must have BOTH 'admin' AND 'auditor' roles (AND logic)
			return [];
		}

		@Permissions('report.export')
		@Get('reports/export')
		exportReport(@AuthToken() token: string) {
			// token is the raw Bearer token without the "Bearer " prefix
			return { token };
		}

		@RequireAllPermissions('data.read', 'data.export')
		@Get('data/export')
		exportData() {
			// User must have BOTH 'data.read' AND 'data.export' permissions (AND logic)
			return [];
		}

		@Get('token-info')
		getTokenInfo(@KeycloakClaims() claims: IKeycloakTokenClaims) {
			// claims contains the decoded JWT payload with all standard OIDC and Keycloak claims
			return { issuer: claims.iss, expiry: claims.exp, scopes: claims.scope };
		}
	}
```

For GraphQL resolvers, use the GraphQL-specific variants:

```ts
	import { Resolver, Query } from '@nestjs/graphql';
	import {
		GraphQLCurrentUser,
		GraphQLAuthToken,
		GraphQLKeycloakClaims,
		Roles,
	} from '@pawells/nestjs-auth';
	import type { IKeycloakUser, IKeycloakTokenClaims } from '@pawells/nestjs-auth';

	@Resolver()
	export class ProfileResolver {
		@Roles('user')
		@Query(() => String, { name: 'GetUserId' })
		async getUserId(@GraphQLCurrentUser('id') userId: string): Promise<string> {
			return userId;
		}

		@Query(() => String, { name: 'GetToken' })
		async getToken(@GraphQLAuthToken() token: string): Promise<string> {
			return token;
		}

		@Query(() => Object, { name: 'GetClaimsInfo' })
		async getClaimsInfo(@GraphQLKeycloakClaims() claims: IKeycloakTokenClaims): Promise<Record<string, unknown>> {
			return { issuer: claims.iss, sessionState: claims.session_state };
		}
	}
```

## API Reference

### Module

| Symbol | Kind | Description |
|---|---|---|
| `KeycloakModule` | Class (NestJS module) | Global dynamic module. Register once with `forRoot(options)` or `forRootAsync(options)`. Provides `KeycloakTokenValidationService` and `KEYCLOAK_MODULE_OPTIONS` to the DI container. |

### Guards

#### HTTP / Universal Guards

These guards work across HTTP and GraphQL contexts. Apply them with `@UseGuards()` or register
globally via `APP_GUARD`.

| Symbol | Kind | Description |
|---|---|---|
| `JwtAuthGuard` | Guard | Validates the Bearer token on every request using `KeycloakTokenValidationService`. Attaches `IKeycloakUser` to `request.user` and raw claims to `request.keycloakClaims`. Respects `@Public()`. |
| `RoleGuard` | Guard | Enforces `@Roles()` metadata. Checks realm and client roles (OR logic). Allows access when no roles are required. Must run after `JwtAuthGuard`. |
| `PermissionGuard` | Guard | Enforces `@Permissions()` metadata using roles-as-permissions semantics (OR logic). Must run after `JwtAuthGuard`. |

### Decorators

#### Method Decorators

| Symbol | Kind | Description |
|---|---|---|
| `Public()` | Method decorator | Marks an endpoint as publicly accessible. `JwtAuthGuard` skips authentication entirely. |
| `Auth()` | Method decorator | Explicitly marks an endpoint as requiring authentication (sets `IS_PUBLIC_KEY` to `false`). |
| `Roles(...roles)` | Method decorator | Specifies one or more role names required for access (OR logic). Used by `RoleGuard`. |
| `RequireAllRoles(...roles)` | Method decorator | Specifies one or more role names required for access (AND logic). User must have ALL specified roles. |
| `Permissions(...permissions)` | Method decorator | Specifies one or more permission strings required for access (OR logic). Used by `PermissionGuard`. |
| `RequireAllPermissions(...permissions)` | Method decorator | Specifies one or more permission strings required for access (AND logic). User must have ALL specified permissions. |

#### Parameter Decorators — HTTP / Universal

| Symbol | Kind | Description |
|---|---|---|
| `CurrentUser(property?, options?)` | Param decorator | Injects the authenticated `IKeycloakUser` from the request. Pass a property name to extract a single field (e.g. `@CurrentUser('id')`). Supports HTTP and GraphQL via auto-detection. |
| `AuthToken(options?)` | Param decorator | Injects the raw JWT string from the `Authorization` header (Bearer prefix stripped). Supports HTTP and GraphQL via auto-detection. |
| `KeycloakClaims(options?)` | Param decorator | Injects the raw Keycloak token claims (`IKeycloakTokenClaims`) from the request. Contains decoded JWT payload with standard OIDC and Keycloak-specific claims. Supports HTTP and GraphQL via auto-detection. |

#### Parameter Decorators — GraphQL

| Symbol | Kind | Description |
|---|---|---|
| `GraphQLCurrentUser(property?)` | Param decorator | GraphQL-specific `CurrentUser`. Hardcodes `contextType: 'graphql'`; use in resolvers instead of `CurrentUser` when you want an explicit binding. |
| `GraphQLAuthToken()` | Param decorator | GraphQL-specific `AuthToken`. Hardcodes `contextType: 'graphql'`. |
| `GraphQLKeycloakClaims()` | Param decorator | GraphQL-specific `KeycloakClaims`. Hardcodes `contextType: 'graphql'`. |
| `GraphQLContextParam()` | Param decorator | Injects the entire GraphQL context object (`GqlExecutionContext.getContext()`). |
| `GraphQLUser` | Alias | Alias for `GraphQLCurrentUser`. |

### Service

| Symbol | Kind | Description |
|---|---|---|
| `KeycloakTokenValidationService` | Injectable service | Validates tokens (online introspection or offline JWKS) and extracts user identity. Inject directly when custom validation logic is needed beyond what the guards provide. |

**Methods:**

| Method | Signature | Description |
|---|---|---|
| `ValidateToken` | `(token: string) => Promise<ITokenValidationResult>` | Validates a JWT. Returns `{ valid: true, claims }` on success or `{ valid: false, error }` on failure. |
| `ExtractUser` | `(claims: IKeycloakTokenClaims) => IKeycloakUser` | Maps raw token claims to a normalized `IKeycloakUser` object. |

### Configuration

| Symbol | Kind | Description |
|---|---|---|
| `KEYCLOAK_CONFIG_SCHEMA` | Zod schema | Validates `TKeycloakModuleOptions`. `clientSecret` is required when `validationMode` is `'online'`. |
| `AssertKeycloakConfig(config)` | Function | Asserts that `config` satisfies the schema. Throws `z.ZodError` on failure. |
| `ValidateKeycloakConfig(config)` | Function | Returns `true` if `config` satisfies the schema; `false` otherwise. |
| `KEYCLOAK_MODULE_OPTIONS` | `Symbol` | DI injection token for the raw module options. Inject with `@Inject(KEYCLOAK_MODULE_OPTIONS)`. |

### Errors

| Symbol | Kind | Description |
|---|---|---|
| `KeycloakAuthError` | Error class | Raised when module initialization fails (invalid config) or token validation encounters a fatal error. Extends `BaseError` with a machine-readable `code` and optional `cause` chain. Keycloak-specific codes are prefixed with `KEYCLOAK_`. |

### Interfaces and Types

| Symbol | Kind | Description |
|---|---|---|
| `TKeycloakModuleOptions` | Type | Configuration options passed to `KeycloakModule.forRoot()` / `forRootAsync()`. Type-aliased to `TKeycloakConfig`. |
| `IKeycloakModuleAsyncOptions` | Interface | Async factory options for `KeycloakModule.forRootAsync()`. Accepts `useFactory`, `inject`, and `imports`. |
| `IKeycloakTokenClaims` | Interface | Raw claims decoded from a Keycloak-issued JWT. Includes standard OIDC claims (`sub`, `iss`, `aud`, `exp`, etc.) and Keycloak-specific fields (`realm_access`, `resource_access`). Supports arbitrary custom claims via index signature. |
| `IKeycloakUser` | Interface | Normalized user identity extracted from validated token claims. Contains `id`, `email`, `username`, `name`, `realmRoles`, and `clientRoles`. Attached to `request.user` by `JwtAuthGuard`. |
| `ITokenValidationResult` | Interface | Return type of `KeycloakTokenValidationService.ValidateToken`. Contains `valid`, optional `claims`, and optional `error` string. |
| `TKeycloakConfig` | Type | Inferred type of `KEYCLOAK_CONFIG_SCHEMA`. |
| `TKeycloakErrorMetadata` | Type | Error metadata type for `KeycloakAuthError`. Extends base metadata with optional `keycloakCode`. |
| `IContextOptions` | Interface | Options for context-aware decorators and utilities. Controls `contextType` and `autoDetect` behaviour. |

### Module Options

Pass configuration to `KeycloakModule.forRoot()` or `forRootAsync()`. All fields are validated at initialization time via Zod.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `authServerUrl` | `string` | Required | Keycloak base URL (e.g., `https://auth.example.com`) |
| `realm` | `string` | Required | Keycloak realm name |
| `clientId` | `string` | Required | This service's client ID in Keycloak |
| `clientSecret` | `string` | Required (online mode) | Client secret for introspection. Required when `validationMode` is `'online'`. |
| `validationMode` | `'online' \| 'offline'` | `'online'` | **Online**: introspection endpoint (detects revocation, requires network). **Offline**: JWT validation via JWKS (faster, stateless, no revocation detection). |
| `issuer` | `string` | Derived | Expected token issuer URL; defaults to `{authServerUrl}/realms/{realm}`. |
| `jwksCacheTtlMs` | `number` | `300000` | JWKS cache TTL in milliseconds (offline mode only). |
| `clockToleranceSec` | `number` | Unset | Clock tolerance in seconds for JWT expiry validation; handles NTP drift (offline mode only). |
| `introspectionTimeoutMs` | `number` | `5000` | HTTP timeout for introspection requests in milliseconds. |
| `introspectionCacheTtlMs` | `number` | Unset | Cache TTL for introspection results in milliseconds. |
| `failedIntrospectionCacheTtlMs` | `number` | Unset | Cache TTL for **failed** introspection results in milliseconds (negative caching, online mode only). When set, invalid/expired/revoked tokens are cached (keyed by SHA-256 hash of the token) for the given TTL to reduce repeated calls to Keycloak for the same bad token. Disabled by default. Use a short TTL (e.g. `60000`) — a longer one risks returning a stale "invalid" result for a token that later becomes valid. |
| `mapUserFn` | `(claims: Record<string, unknown>, user: IKeycloakUser) => IKeycloakUser` | Unset | Optional hook to reshape the extracted `IKeycloakUser` after successful validation, called from `ExtractUser()`. Supports non-standard Keycloak protocol mappers (custom claims, multi-tenant claim namespacing, etc). Only invoked on success; if the function throws, the exception propagates rather than being swallowed. |

**Example: Offline mode with clock tolerance**

```ts
KeycloakModule.forRoot({
	authServerUrl: 'https://auth.example.com',
	realm: 'myrealm',
	clientId: 'my-client',
	validationMode: 'offline',
	clockToleranceSec: 60,  // Allow tokens expired up to 60s ago
	jwksCacheTtlMs: 600_000, // Cache JWKS for 10 minutes
})
```

**Example: Negative caching and custom claims mapping**

```ts
KeycloakModule.forRoot({
	authServerUrl: 'https://auth.example.com',
	realm: 'myrealm',
	clientId: 'my-client',
	clientSecret: 'secret',
	validationMode: 'online',
	failedIntrospectionCacheTtlMs: 60_000, // Cache invalid-token results for 1 minute
	mapUserFn: (claims, user) => ({
		...user,
		tenantId: claims['urn:custom:tenant_id'],
	}),
})
```

### Observability

`KeycloakTokenValidationService` records two metrics via `@pawells/metrics`, registered idempotently on service construction:

| Metric | Type | Labels | Description |
|---|---|---|---|
| `pawells_auth_token_validation_duration_seconds` | Histogram | `mode` (`online`\|`offline`), `result` (`valid`\|`invalid`) | Token validation latency (5ms–10s buckets). |
| `pawells_auth_introspection_cache_total` | Counter | `result` (`hit`\|`miss`\|`negative_hit`) | Introspection cache performance during online validation. |

Recording is wrapped in try/catch and never breaks token validation. These metrics are only collected if an exporter is registered with `@pawells/metrics`'s `Instrumentation` registry — typically via `@pawells/nestjs-metrics` in your application bootstrap. Without a registered exporter, recording is a safe no-op.

### Context Utilities

| Symbol | Kind | Description |
|---|---|---|
| `DetectContextType(ctx)` | Function | Returns `'http'`, `'graphql'`, or `'websocket'` for the given `ExecutionContext`. |
| `ExtractRequestFromContext(ctx, options?)` | Function | Extracts the augmented `Request` object from HTTP, GraphQL, or WebSocket contexts. |
| `ExtractUserFromContext(ctx, options?)` | Function | Extracts the authenticated user from any supported execution context. |
| `ExtractAuthTokenFromContext(ctx, options?)` | Function | Extracts the Bearer token string from any supported execution context. |

### Metadata Keys

| Symbol | Kind | Description |
|---|---|---|
| `IS_PUBLIC_KEY` | `string` | Metadata key used by `@Public()` / `@Auth()` and read by `JwtAuthGuard`. |
| `ROLES_KEY` | `string` | Metadata key used by `@Roles()` (OR logic) and read by `RoleGuard`. |
| `REQUIRE_ALL_ROLES_KEY` | `string` | Metadata key used by `@RequireAllRoles()` (AND logic) and read by `RoleGuard`. |
| `PERMISSIONS_KEY` | `string` | Metadata key used by `@Permissions()` (OR logic) and read by `PermissionGuard`. |
| `REQUIRE_ALL_PERMISSIONS_KEY` | `string` | Metadata key used by `@RequireAllPermissions()` (AND logic) and read by `PermissionGuard`. |

### Testing Utilities

The `@pawells/nestjs-auth/testing` subpath export provides mock implementations for unit testing without requiring a live Keycloak server:

```ts
import { KeycloakTestingModule, MockKeycloakTokenValidationService, CreateMockKeycloakUser } from '@pawells/nestjs-auth/testing';
```

**Exports:**

| Symbol | Kind | Description |
|---|---|---|
| `KeycloakTestingModule` | Class (NestJS module) | Drop-in replacement for `KeycloakModule` in test suites. Provides `MockKeycloakTokenValidationService` with configurable stubs. Call `forRoot(options?)` to register. |
| `IKeycloakTestingModuleOptions` | Interface | Sync options for `KeycloakTestingModule.forRoot()`. |
| `IKeycloakTestingModuleAsyncOptions` | Interface | Async factory options for `KeycloakTestingModule.forRootAsync()`. |
| `MockKeycloakTokenValidationService` | Injectable service | Configurable stub for `KeycloakTokenValidationService`. Defaults to valid token results with test defaults. Use `SetValidateTokenResult()` and `SetExtractUserResult()` to override behavior per-test. Use `Reset()` to restore defaults. |
| `CreateMockKeycloakUser(overrides?)` | Function | Factory for `IKeycloakUser` with sensible test defaults. Pass `Partial<IKeycloakUser>` to override individual fields. Pure, side-effect-free — safe in any test context. |

## License

MIT — Phillip Aaron Wells. See [LICENSE](./LICENSE) for details.
