{"_id":"@elchinabilov/nestjs-auth","_rev":"2-d25e3c315928a9d8293ef6e5576c67b8","name":"@elchinabilov/nestjs-auth","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@elchinabilov/nestjs-auth","version":"1.0.0","keywords":["nestjs","auth","authentication","authorization","jwt","cookie","session","oauth","otp","magic-link","2fa","totp","rbac","api-key"],"author":{"name":"Elchin Abilov"},"license":"MIT","_id":"@elchinabilov/nestjs-auth@1.0.0","maintainers":[{"name":"abilov","email":"abilovelchin@gmail.com"}],"dist":{"shasum":"63221710bd1a0a7e4800d2a3cbc6e3695497c0d4","tarball":"https://registry.npmjs.org/@elchinabilov/nestjs-auth/-/nestjs-auth-1.0.0.tgz","fileCount":235,"integrity":"sha512-OIF3QBxCnc5uiV4DtdHWfI5GGyOGcXDz/Woi93gce4jml2XeCU5OOZKB4CLyjc8jdEab85QixAxS54b7P0F5+A==","signatures":[{"sig":"MEQCIFBWnJU17FQTZp2K2+SRQ0eoLoX2oWpFKlboQQrwjD00AiAIY5HgYnL8quE24YPpaTWZfLBh8kB5ih1bImHn/xEQSA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":303756},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"gitHead":"71fe64e33d406a7b15435f34a63b98c2b936e226","scripts":{"lint":"eslint \"src/**/*.ts\"","test":"jest","build":"tsc -p tsconfig.build.json","clean":"rimraf dist *.tsbuildinfo","format":"prettier --write \"src/**/*.ts\"","test:cov":"jest --coverage","test:watch":"jest --watch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"abilov","email":"abilovelchin@gmail.com"},"_npmVersion":"11.7.0","description":"Modular, configurable and production-ready authentication system for NestJS: cookie, bearer/JWT, session, API key, OAuth, OTP, magic link, 2FA and RBAC.","directories":{},"_nodeVersion":"22.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","rxjs":"^7.8.1","eslint":"^8.57.0","rimraf":"^5.0.0","ts-jest":"^29.2.0","prettier":"^3.3.0","typescript":"^5.5.0","@nestjs/jwt":"^11.0.0","@types/jest":"^29.5.12","@types/node":"^22.0.0","@nestjs/core":"^11.0.0","@nestjs/common":"^11.0.0","@nestjs/testing":"^11.0.0","reflect-metadata":"^0.2.2","@typescript-eslint/parser":"^7.0.0","@typescript-eslint/eslint-plugin":"^7.0.0"},"peerDependencies":{"rxjs":"^7.0.0","@nestjs/jwt":"^10.0.0 || ^11.0.0","@nestjs/core":"^10.0.0 || ^11.0.0","@nestjs/common":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0"},"peerDependenciesMeta":{"passport":{"optional":true},"@nestjs/passport":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/nestjs-auth_1.0.0_1781088898498_0.292485814053423","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@elchinabilov/nestjs-auth","version":"2.0.0","description":"Modular, configurable and production-ready authentication system for NestJS: cookie, bearer/JWT, session, API key, OAuth, OTP, magic link, 2FA and RBAC.","author":{"name":"Elchin Abilov"},"license":"MIT","keywords":["nestjs","auth","authentication","authorization","jwt","cookie","session","oauth","otp","magic-link","2fa","totp","rbac","api-key"],"main":"dist/index.js","types":"dist/index.d.ts","publishConfig":{"access":"public"},"scripts":{"build":"tsc -p tsconfig.build.json","clean":"rimraf dist *.tsbuildinfo","prepublishOnly":"npm run clean && npm run build","lint":"eslint \"src/**/*.ts\"","format":"prettier --write \"src/**/*.ts\"","test":"jest","test:watch":"jest --watch","test:cov":"jest --coverage"},"peerDependencies":{"@nestjs/common":"^10.0.0 || ^11.0.0","@nestjs/core":"^10.0.0 || ^11.0.0","@nestjs/jwt":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0","rxjs":"^7.0.0"},"peerDependenciesMeta":{"@nestjs/passport":{"optional":true},"passport":{"optional":true}},"devDependencies":{"@nestjs/common":"^11.0.0","@nestjs/core":"^11.0.0","@nestjs/jwt":"^11.0.0","@nestjs/testing":"^11.0.0","@types/jest":"^29.5.12","@types/node":"^22.0.0","@typescript-eslint/eslint-plugin":"^7.0.0","@typescript-eslint/parser":"^7.0.0","eslint":"^8.57.0","jest":"^29.7.0","prettier":"^3.3.0","reflect-metadata":"^0.2.2","rimraf":"^5.0.0","rxjs":"^7.8.1","ts-jest":"^29.2.0","typescript":"^5.5.0"},"engines":{"node":">=18"},"gitHead":"2cf522e9318c239531b08dca56cde49032156493","_id":"@elchinabilov/nestjs-auth@2.0.0","_nodeVersion":"22.19.0","_npmVersion":"11.7.0","dist":{"integrity":"sha512-oYKwPeA9pH9idrAgasXfy4evuZPyWBCBDwciLxHT8R2iNIEJ4ydKWRB58hPfdZ66DRXA7hDzncJe35HuAu0DgA==","shasum":"cbef6d50d8884fcc7261704192751758e66a3a4e","tarball":"https://registry.npmjs.org/@elchinabilov/nestjs-auth/-/nestjs-auth-2.0.0.tgz","fileCount":251,"unpackedSize":353023,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCvukbKU+XaZTGZPVoJZ+b79EgVQvCqdHG7ZXaIfG5yGgIhAKngeGY+ezUyEm56oZ6IaZgCUqHFeEystuppDtXjFGX0"}]},"_npmUser":{"name":"abilov","email":"abilovelchin@gmail.com"},"directories":{},"maintainers":[{"name":"abilov","email":"abilovelchin@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs-auth_2.0.0_1781333021984_0.8581998153014434"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-10T10:54:58.326Z","modified":"2026-06-13T06:43:42.261Z","1.0.0":"2026-06-10T10:54:58.649Z","2.0.0":"2026-06-13T06:43:42.150Z"},"author":{"name":"Elchin Abilov"},"license":"MIT","keywords":["nestjs","auth","authentication","authorization","jwt","cookie","session","oauth","otp","magic-link","2fa","totp","rbac","api-key"],"description":"Modular, configurable and production-ready authentication system for NestJS: cookie, bearer/JWT, session, API key, OAuth, OTP, magic link, 2FA and RBAC.","maintainers":[{"name":"abilov","email":"abilovelchin@gmail.com"}],"readme":"# @elchinabilov/nestjs-auth\n\nA modular, configurable and **production-ready authentication system for NestJS**.\nOne package, every common strategy — cookie, bearer/JWT, server-side sessions,\nAPI keys, OAuth, OTP, **reverse OTP (inbound WhatsApp / Telegram)**, magic links,\nTOTP 2FA — plus a full RBAC/permissions layer and multi-device session\nmanagement.\n\nThe library is **ORM-agnostic** and **adapter-driven**: you plug in your own\ndatabase, cache, mail and SMS implementations. It ships **no controllers and no\nroutes** — you stay in control of your API surface and call the provided\nservices and guards from your own code.\n\n---\n\n## Table of contents\n\n- [Install](#install)\n- [Quick start](#quick-start)\n- [Configuration](#configuration)\n  - [`forRoot`](#forroot)\n  - [`forRootAsync` with ConfigModule](#forrootasync-with-configmodule)\n- [Adapters](#adapters)\n- [Authentication methods](#authentication-methods)\n  - [1. Cookie-based](#1-cookie-based-authentication)\n  - [2. Bearer / JWT](#2-bearer--jwt-authentication)\n  - [3. Sessions](#3-session-based-authentication)\n  - [4. API keys](#4-api-key-authentication)\n  - [5. OAuth (Google / GitHub)](#5-oauth-authentication)\n  - [6. OTP (email / SMS)](#6-otp-authentication)\n  - [7. Reverse OTP (WhatsApp / Telegram)](#7-reverse-otp-authentication)\n  - [8. Magic links](#8-magic-link-authentication)\n  - [9. Two-factor (TOTP)](#9-two-factor-authentication)\n- [Authorization (RBAC + permissions)](#authorization)\n- [Device & session management](#device--session-management)\n- [Guards, decorators & services reference](#api-reference)\n- [Error handling](#error-handling)\n- [Testing](#testing)\n\n---\n\n## Install\n\n```bash\nnpm install @elchinabilov/nestjs-auth\n# peer deps (you very likely already have these)\nnpm install @nestjs/common @nestjs/core @nestjs/jwt reflect-metadata rxjs\n```\n\nFor cookie auth, enable `cookie-parser` in your bootstrap:\n\n```ts\nimport * as cookieParser from 'cookie-parser';\napp.use(cookieParser(process.env.COOKIE_SECRET)); // secret only needed for signed cookies\n```\n\n> No native dependencies. Password hashing (scrypt), TOTP (HMAC-SHA1) and all\n> token/crypto primitives use Node's built-in `crypto`.\n\n---\n\n## Quick start\n\n```ts\n// auth.config.ts\nimport { AuthModule } from '@elchinabilov/nestjs-auth';\nimport { Module } from '@nestjs/common';\nimport { PrismaUserAdapter } from './prisma-user.adapter';\n\n@Module({\n  imports: [\n    AuthModule.forRoot({\n      global: true,\n      jwt: {\n        secret: process.env.JWT_SECRET!,\n        accessToken: { expiresIn: '15m' },\n        refreshToken: { expiresIn: '7d' },\n      },\n      cookie: { enabled: true, secure: true, sameSite: 'lax' },\n      session: { enabled: true, ttl: 60 * 60 * 24 * 7, maxPerUser: 5 },\n      userAdapter: PrismaUserAdapter,\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n```ts\n// auth.controller.ts\nimport { Controller, Post, Body, Req, Res, UseGuards } from '@nestjs/common';\nimport {\n  AuthService,\n  AuthGuard,\n  CurrentUser,\n  Public,\n  AuthUser,\n} from '@elchinabilov/nestjs-auth';\n\n@Controller('auth')\nexport class AuthController {\n  constructor(private readonly auth: AuthService) {}\n\n  @Public()\n  @Post('login')\n  async login(@Body() dto: LoginDto, @Req() req, @Res({ passthrough: true }) res) {\n    const { user, tokens } = await this.auth.loginWithCredentials(\n      dto.email,\n      dto.password,\n      { request: req, response: res }, // sets HttpOnly cookies automatically\n    );\n    return { user: { id: user.id, email: user.email }, tokens };\n  }\n\n  @UseGuards(AuthGuard)\n  @Post('logout')\n  async logout(@CurrentUser() user: AuthUser, @Req() req, @Res({ passthrough: true }) res) {\n    await this.auth.logout({ sessionId: req.user.sessionId, response: res });\n    return { ok: true };\n  }\n}\n```\n\n---\n\n## Configuration\n\n### `forRoot`\n\nEvery section is optional except `jwt`. Defaults shown below are applied\nautomatically by `AuthConfigService`.\n\n```ts\nAuthModule.forRoot({\n  global: false,                       // register guards/services app-wide\n\n  jwt: {\n    secret: process.env.JWT_SECRET!,   // or privateKey/publicKey for RS256\n    issuer: 'my-app',\n    audience: 'my-app-clients',\n    accessToken:  { expiresIn: '15m' },// default 15m\n    refreshToken: { expiresIn: '7d' }, // default 7d, can use a separate secret\n  },\n\n  cookie: {\n    enabled: true,\n    accessTokenName: 'access_token',\n    refreshTokenName: 'refresh_token',\n    httpOnly: true,                    // default true\n    secure: true,                      // default true\n    sameSite: 'lax',                   // 'lax' | 'strict' | 'none'\n    domain: '.example.com',\n    path: '/',\n    signed: false,\n  },\n\n  csrf: {\n    enabled: true,                     // double-submit-cookie protection\n    cookieName: 'csrf_token',\n    headerName: 'x-csrf-token',\n    protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],\n  },\n\n  session: {\n    enabled: true,\n    ttl: 60 * 60 * 24 * 7,             // seconds\n    rolling: true,                     // refresh expiry on activity\n    maxPerUser: 5,                     // 0 = unlimited; evicts oldest beyond cap\n  },\n\n  apiKey: {\n    enabled: true,\n    header: 'x-api-key',\n    prefix: 'sk_',\n    keys: { 'sk_live_123': { name: 'billing-svc', roles: ['service'] } },\n    // or: validate: async (key) => db.apiKeys.findActive(key),\n  },\n\n  otp: { length: 6, ttl: 300, maxRetries: 5, resendCooldown: 60, alphanumeric: false },\n\n  // Reverse / inbound OTP — the user sends a server-issued code BACK over a\n  // chat channel. Inert unless `enabled`; register an adapter per channel.\n  reverseOtp: { enabled: true, length: 6, ttl: 300, alphanumeric: false },\n  inboundAdapters: [\n    new TelegramInboundAdapter({ secretToken: process.env.TG_WEBHOOK_SECRET }),\n    new MyTwilioWhatsAppAdapter(), // your subclass of WhatsAppInboundAdapter\n  ],\n\n  magicLink: { ttl: 900, baseUrl: 'https://app.com/auth/magic', tokenParam: 'token' },\n\n  twoFactor: { issuer: 'My App', window: 1, backupCodeCount: 10 },\n\n  oauth: {\n    google: { clientId: '...', clientSecret: '...', callbackUrl: 'https://app.com/auth/google/callback' },\n    github: { clientId: '...', clientSecret: '...', callbackUrl: 'https://app.com/auth/github/callback' },\n  },\n\n  // Adapters (class or ready instance)\n  userAdapter: MyUserAdapter,\n  cache: new RedisCacheAdapter(redis),  // defaults to in-memory if omitted\n  mail: MyMailAdapter,\n  sms: MyTwilioAdapter,\n});\n```\n\n### `forRootAsync` with ConfigModule\n\n```ts\nAuthModule.forRootAsync({\n  imports: [ConfigModule],\n  inject: [ConfigService],\n  useFactory: (config: ConfigService) => ({\n    jwt: { secret: config.getOrThrow('JWT_SECRET') },\n    cookie: { enabled: true, secure: config.get('NODE_ENV') === 'production' },\n    session: { enabled: true },\n  }),\n});\n```\n\n`useClass` / `useExisting` factories implementing `AuthOptionsFactory` are also\nsupported. When using `forRootAsync`, prefer passing **adapter instances** (not\nclasses) so they are wired exactly as you construct them.\n\n---\n\n## Adapters\n\nAdapters are how the package talks to your infrastructure. Only `UserAdapter` is\nneeded for stateful flows; the cache adapter defaults to in-memory.\n\n| Token | Interface | Required for |\n| --- | --- | --- |\n| `userAdapter` | `UserAdapter` | credentials login, OAuth linking, 2FA persistence |\n| `cache` | `CacheAdapter` | sessions, OTP, magic links, OAuth state (Redis in prod) |\n| `mail` | `MailAdapter` | email OTP, magic links |\n| `sms` | `SmsAdapter` | SMS OTP |\n| `inboundAdapters` | `InboundChannelAdapter[]` | reverse OTP (inbound WhatsApp / Telegram) |\n\n```ts\n// Example: Prisma user adapter\nimport { Injectable } from '@nestjs/common';\nimport { UserAdapter, AuthUser, PasswordService } from '@elchinabilov/nestjs-auth';\n\n@Injectable()\nexport class PrismaUserAdapter implements UserAdapter {\n  constructor(private prisma: PrismaService, private passwords: PasswordService) {}\n\n  async findById(id: string) {\n    return this.prisma.user.findUnique({ where: { id } }) as Promise<AuthUser | null>;\n  }\n\n  async validateCredentials(email: string, password: string) {\n    const user = await this.prisma.user.findUnique({ where: { email } });\n    if (!user || !(await this.passwords.verify(password, user.passwordHash))) return null;\n    return user as AuthUser;\n  }\n\n  async upsertOAuthUser(profile) {\n    return this.prisma.user.upsert({\n      where: { email: profile.email! },\n      update: {},\n      create: { email: profile.email!, name: profile.displayName },\n    }) as Promise<AuthUser>;\n  }\n}\n```\n\nA `RedisCacheAdapter` is ~20 lines on top of `ioredis` implementing the\n`CacheAdapter` interface (`get/set/del/incr/keys`).\n\n---\n\n## Authentication methods\n\n### 1. Cookie-based authentication\n\nEnable `cookie` options and pass the `response` to login — HttpOnly access &\nrefresh cookies are set with your configured `Secure`/`SameSite`/domain. The\n`AuthGuard` reads them automatically.\n\n```ts\nawait this.auth.login(user, { request: req, response: res }); // sets cookies\nawait this.auth.refresh(req.cookies.refresh_token, { response: res }); // rotates\nawait this.auth.logout({ sessionId: req.user.sessionId, response: res }); // clears\n```\n\n**Refresh token rotation** is built in (see [sessions](#3-session-based-authentication)).\n**CSRF**: enable `csrf` and issue a token with `CsrfService.issue(res)`; the\nguard validates the header against the cookie on state-changing requests.\n\n### 2. Bearer / JWT authentication\n\n```ts\n@UseGuards(AuthGuard)\n@Get('me')\nme(@CurrentUser() user: AuthUser) { return user; }\n```\n\n`Authorization: Bearer <token>` is validated against the access-token secret.\nAccess and refresh tokens may use **separate secrets and lifetimes**; the `type`\nclaim prevents using one as the other. Restrict a route to specific strategies\nwith `@AuthMethods(AuthMethod.BEARER)`.\n\n### 3. Session-based authentication\n\nWhen `session.enabled`, login creates a server-side session (in your cache\nadapter) and embeds its id (`sid`) in the JWT. Every request re-validates the\nsession, so you can **revoke tokens server-side** — true logout, not just an\nexpiring JWT. Supports multi-device, rolling expiry, `maxPerUser` caps and\n**refresh-token rotation with reuse detection** (a replayed refresh token kills\nthe session).\n\n### 4. API key authentication\n\n```ts\n@UseGuards(ApiKeyGuard)            // x-api-key header\n@Get('internal/metrics')\nmetrics() {}\n\n@UseGuards(ApiKeyOrAuthGuard)      // accept a key OR a user token\n@Get('data')\ndata() {}\n```\n\nKeys resolve to a principal carrying `roles`/`permissions`, so RBAC guards work\nfor services too. Use a static `keys` map or an async `validate` callback.\n\n### 5. OAuth authentication\n\n```ts\n@Public() @Get('google')\nasync google(@Res() res) {\n  const { url } = await this.oauth.getAuthorizationUrl('google', {\n    redirectUrl: '/dashboard',\n  });\n  res.redirect(url);\n}\n\n@Public() @Get('google/callback')\nasync callback(@Query('code') code, @Query('state') state, @Req() req, @Res({ passthrough: true }) res) {\n  const { profile, redirectUrl } = await this.oauth.handleCallback('google', code, state);\n  const user = await this.userAdapter.upsertOAuthUser(profile);\n  return this.auth.login(user, { request: req, response: res });\n}\n```\n\nBuilt-in **Google** and **GitHub** providers; the `state` nonce is stored in the\ncache for CSRF protection. Add any provider by implementing `OAuthProvider` and\nregistering it (config under `oauth.<name>`, then `oauthService.registerProvider`).\n\n### 6. OTP authentication\n\n```ts\nawait this.otp.send({ channel: OtpChannel.EMAIL, destination: 'a@b.com' });\nawait this.otp.verify('a@b.com', code, OtpPurpose.LOGIN); // throws on bad/expired\n```\n\nNumeric or alphanumeric codes, configurable length/TTL, **per-code retry limit**,\n**resend cooldown**, single-use, hashed at rest. Email works out of the box;\n**SMS is an optional adapter** (`sms`).\n\n### 7. Reverse OTP authentication\n\n**Reverse (inbound) OTP** flips the usual flow: instead of the server delivering\na code, the server issues a code and the **user sends it back** from their own\nWhatsApp or Telegram. That inbound message proves the user controls the\naccount/number. The package mints + verifies the code and tracks the session;\n**it never sends anything outbound** and stays provider-agnostic — provider\nlogic lives entirely in your inbound adapters.\n\n```ts\n// 1. Create a verification session and show the code + target to the user.\n//    e.g. \"Send TELEGRAM the code 482913 to @my_login_bot\"\nconst { sessionId, code } = await this.reverseOtp.createSession({\n  channel: ReverseOtpChannel.TELEGRAM,\n  identifier: pendingUserId,         // optional app-level binding\n});\n\n// 2. In your webhook route, hand the raw request to the service. The matching\n//    adapter authenticates + normalizes it, then any code is matched.\n@Public() @Post('webhooks/telegram')\nasync telegram(@Body() body, @Headers() headers) {\n  await this.reverseOtp.handleInbound(ReverseOtpChannel.TELEGRAM, { body, headers });\n  return { ok: true };\n}\n\n// 3. Poll (or react) on the session status to complete your login flow.\nconst status = await this.reverseOtp.getStatus(sessionId); // pending | approved | denied | expired\nif (status === ReverseOtpStatus.APPROVED) {\n  const session = await this.reverseOtp.getSession(sessionId);\n  // session.verifiedSender → the proven phone number / Telegram user id\n}\n```\n\nState lives entirely in the **cache adapter** (no database), codes are\n**single-use and hashed at rest**, and the feature is **inert unless\n`reverseOtp.enabled`** and an adapter for the channel is registered.\n\n**Telegram** works out of the box via `TelegramInboundAdapter`, which normalizes\nthe official Bot API `Update` payload and (optionally) verifies the\n`X-Telegram-Bot-Api-Secret-Token` header. **WhatsApp** is provider-agnostic:\nextend `WhatsAppInboundAdapter` for Twilio, Meta Cloud API, Infobip, 360dialog,\netc.\n\n```ts\n// Provider-specific WhatsApp adapter (example: Twilio)\nimport { WhatsAppInboundAdapter, InboundWebhookRequest } from '@elchinabilov/nestjs-auth';\n\nexport class MyTwilioWhatsAppAdapter extends WhatsAppInboundAdapter {\n  parse(req: InboundWebhookRequest) {\n    const b = req.body as { From?: string; Body?: string; MessageSid?: string };\n    if (!b?.From || !b?.Body) return [];\n    return [this.message({\n      from: b.From.replace(/^whatsapp:/, ''),\n      text: b.Body,\n      messageId: b.MessageSid,\n      raw: b,\n    })];\n  }\n  // optional: override verify(req) to check the X-Twilio-Signature header\n}\n```\n\nEvery adapter returns the same normalized `InboundMessage`\n(`channel`, `from`, `text`, `messageId?`, `senderName?`, `timestamp?`, `raw?`),\nso the core never depends on a provider. A webhook may yield several messages;\n`handleInbound` returns one result per message. Register adapters at startup via\n`inboundAdapters`, or at runtime with `reverseOtp.registerAdapter(adapter)`.\nCustom code extraction from message text is configurable via\n`reverseOtp.extractCodes`.\n\n### 8. Magic link authentication\n\n```ts\nawait this.magicLink.sendToEmail('a@b.com', { redirectUrl: '/welcome' });\n// on click:\nconst { identifier, redirectUrl } = await this.magicLink.verify(token);\nconst user = await this.userAdapter.findByEmail!(identifier);\nawait this.auth.login(user, { request: req, response: res });\n```\n\nTokens are **one-time-use**, time-limited, hashed at rest, with optional\nredirect URL support.\n\n### 9. Two-factor authentication\n\n```ts\n// enable\nconst e = this.twoFactor.generateEnrollment(user.email);\nawait this.userAdapter.saveTwoFactorSecret!(user.id, e.secret, e.hashedBackupCodes);\nreturn { otpauthUrl: e.otpauthUrl, backupCodes: e.backupCodes }; // show once\n\n// verify (Google Authenticator / Authy / 1Password compatible)\nconst ok = this.twoFactor.verifyToken(secret, code)\n  || !!this.twoFactor.verifyBackupCode(code, hashedBackupCodes);\n```\n\nProtect sensitive routes with `@RequireTwoFactor()` + `TwoFactorGuard`. Mark a\nsession 2FA-verified via `sessionService.markTwoFactorVerified(sid)` or by\npassing `twoFactorVerified: true` to `auth.login`.\n\n---\n\n## Authorization\n\n```ts\n@UseGuards(AuthGuard, RolesGuard, PermissionsGuard)\n@Roles('admin', 'editor')           // any of these roles\n@Permissions('posts:write')         // all listed permissions\n@Post('posts')\ncreate() {}\n\n@AnyPermission('posts:read', 'posts:write') // at least one\n@Get('posts')\nlist() {}\n```\n\n- `@Roles(...)` → `RolesGuard` (RBAC, any-of)\n- `@Permissions(...)` → `PermissionsGuard` (all-of), `@AnyPermission(...)` (any-of)\n- `@Public()` → bypass auth on a route even under a global guard\n- `@CurrentUser()` / `@CurrentUser('id')` → inject the user / a field\n- `@AuthContext()` → inject `{ user, sessionId, method, device, twoFactorVerified }`\n\nRegister globally with `APP_GUARD` for secure-by-default:\n\n```ts\n{ provide: APP_GUARD, useClass: AuthGuard }\n```\n\n## Device & session management\n\n```ts\nthis.sessionService.listForUser(userId);            // active sessions (multi-device)\nthis.sessionService.revoke(sessionId);              // sign out one device\nthis.sessionService.revokeAllForUser(userId, keep); // sign out all (except current)\nthis.deviceService.extract(req);                    // { ip, userAgent, browser, os, deviceType, fingerprint }\n```\n\nIP and user-agent are tracked per session (best-effort parsing, no extra deps).\n\n## API reference\n\n**Services:** `AuthService`, `TokenService`, `PasswordService`, `CookieService`,\n`CsrfService`, `SessionService`, `ApiKeyService`, `OtpService`,\n`ReverseOtpService`, `MagicLinkService`, `TwoFactorService`, `DeviceService`,\n`OAuthService`, `AuthUserService`, `AuthConfigService`.\n\n**Guards:** `AuthGuard`, `ApiKeyGuard`, `ApiKeyOrAuthGuard`, `RolesGuard`,\n`PermissionsGuard`, `TwoFactorGuard`.\n\n**Decorators:** `@Public`, `@Roles`, `@Permissions`, `@AnyPermission`,\n`@CurrentUser`, `@AuthContext`, `@AuthMethods`, `@RequireTwoFactor`.\n\nAll are exported from the package root with full TypeScript types.\n\n## Error handling\n\nFailures throw typed exceptions carrying a stable machine `code` and the right\nHTTP status — never leaking secrets or stack traces:\n\n`InvalidCredentialsException`, `InvalidTokenException`, `TokenExpiredException`,\n`MissingTokenException`, `SessionExpiredException`, `InvalidApiKeyException`,\n`OtpException`, `ReverseOtpException`, `TwoFactorRequiredException`,\n`InvalidCsrfTokenException`, `InsufficientPermissionsException`,\n`AuthConfigurationError`.\n\n```jsonc\n// example 401 body\n{ \"statusCode\": 401, \"message\": \"Token has expired\", \"code\": \"token_expired\", \"error\": \"Unauthorized\" }\n```\n\n## Testing\n\nEvery service is a plain injectable with explicit dependencies — trivial to\nunit-test. The package itself ships unit tests plus a DI-graph integration test:\n\n```bash\nnpm test\n```\n\n---\n\n## License\n\nMIT © Elchin Abilov\n","readmeFilename":"README.md"}