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

@elchinabilov/nestjs-auth

v2.0.0

Published

Modular, configurable and production-ready authentication system for NestJS: cookie, bearer/JWT, session, API key, OAuth, OTP, magic link, 2FA and RBAC.

Readme

@elchinabilov/nestjs-auth

A modular, configurable and production-ready authentication system for NestJS. One package, every common strategy — cookie, bearer/JWT, server-side sessions, API keys, OAuth, OTP, reverse OTP (inbound WhatsApp / Telegram), magic links, TOTP 2FA — plus a full RBAC/permissions layer and multi-device session management.

The library is ORM-agnostic and adapter-driven: you plug in your own database, cache, mail and SMS implementations. It ships no controllers and no routes — you stay in control of your API surface and call the provided services and guards from your own code.


Table of contents


Install

npm install @elchinabilov/nestjs-auth
# peer deps (you very likely already have these)
npm install @nestjs/common @nestjs/core @nestjs/jwt reflect-metadata rxjs

For cookie auth, enable cookie-parser in your bootstrap:

import * as cookieParser from 'cookie-parser';
app.use(cookieParser(process.env.COOKIE_SECRET)); // secret only needed for signed cookies

No native dependencies. Password hashing (scrypt), TOTP (HMAC-SHA1) and all token/crypto primitives use Node's built-in crypto.


Quick start

// auth.config.ts
import { AuthModule } from '@elchinabilov/nestjs-auth';
import { Module } from '@nestjs/common';
import { PrismaUserAdapter } from './prisma-user.adapter';

@Module({
  imports: [
    AuthModule.forRoot({
      global: true,
      jwt: {
        secret: process.env.JWT_SECRET!,
        accessToken: { expiresIn: '15m' },
        refreshToken: { expiresIn: '7d' },
      },
      cookie: { enabled: true, secure: true, sameSite: 'lax' },
      session: { enabled: true, ttl: 60 * 60 * 24 * 7, maxPerUser: 5 },
      userAdapter: PrismaUserAdapter,
    }),
  ],
})
export class AppModule {}
// auth.controller.ts
import { Controller, Post, Body, Req, Res, UseGuards } from '@nestjs/common';
import {
  AuthService,
  AuthGuard,
  CurrentUser,
  Public,
  AuthUser,
} from '@elchinabilov/nestjs-auth';

@Controller('auth')
export class AuthController {
  constructor(private readonly auth: AuthService) {}

  @Public()
  @Post('login')
  async login(@Body() dto: LoginDto, @Req() req, @Res({ passthrough: true }) res) {
    const { user, tokens } = await this.auth.loginWithCredentials(
      dto.email,
      dto.password,
      { request: req, response: res }, // sets HttpOnly cookies automatically
    );
    return { user: { id: user.id, email: user.email }, tokens };
  }

  @UseGuards(AuthGuard)
  @Post('logout')
  async logout(@CurrentUser() user: AuthUser, @Req() req, @Res({ passthrough: true }) res) {
    await this.auth.logout({ sessionId: req.user.sessionId, response: res });
    return { ok: true };
  }
}

Configuration

forRoot

Every section is optional except jwt. Defaults shown below are applied automatically by AuthConfigService.

AuthModule.forRoot({
  global: false,                       // register guards/services app-wide

  jwt: {
    secret: process.env.JWT_SECRET!,   // or privateKey/publicKey for RS256
    issuer: 'my-app',
    audience: 'my-app-clients',
    accessToken:  { expiresIn: '15m' },// default 15m
    refreshToken: { expiresIn: '7d' }, // default 7d, can use a separate secret
  },

  cookie: {
    enabled: true,
    accessTokenName: 'access_token',
    refreshTokenName: 'refresh_token',
    httpOnly: true,                    // default true
    secure: true,                      // default true
    sameSite: 'lax',                   // 'lax' | 'strict' | 'none'
    domain: '.example.com',
    path: '/',
    signed: false,
  },

  csrf: {
    enabled: true,                     // double-submit-cookie protection
    cookieName: 'csrf_token',
    headerName: 'x-csrf-token',
    protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
  },

  session: {
    enabled: true,
    ttl: 60 * 60 * 24 * 7,             // seconds
    rolling: true,                     // refresh expiry on activity
    maxPerUser: 5,                     // 0 = unlimited; evicts oldest beyond cap
  },

  apiKey: {
    enabled: true,
    header: 'x-api-key',
    prefix: 'sk_',
    keys: { 'sk_live_123': { name: 'billing-svc', roles: ['service'] } },
    // or: validate: async (key) => db.apiKeys.findActive(key),
  },

  otp: { length: 6, ttl: 300, maxRetries: 5, resendCooldown: 60, alphanumeric: false },

  // Reverse / inbound OTP — the user sends a server-issued code BACK over a
  // chat channel. Inert unless `enabled`; register an adapter per channel.
  reverseOtp: { enabled: true, length: 6, ttl: 300, alphanumeric: false },
  inboundAdapters: [
    new TelegramInboundAdapter({ secretToken: process.env.TG_WEBHOOK_SECRET }),
    new MyTwilioWhatsAppAdapter(), // your subclass of WhatsAppInboundAdapter
  ],

  magicLink: { ttl: 900, baseUrl: 'https://app.com/auth/magic', tokenParam: 'token' },

  twoFactor: { issuer: 'My App', window: 1, backupCodeCount: 10 },

  oauth: {
    google: { clientId: '...', clientSecret: '...', callbackUrl: 'https://app.com/auth/google/callback' },
    github: { clientId: '...', clientSecret: '...', callbackUrl: 'https://app.com/auth/github/callback' },
  },

  // Adapters (class or ready instance)
  userAdapter: MyUserAdapter,
  cache: new RedisCacheAdapter(redis),  // defaults to in-memory if omitted
  mail: MyMailAdapter,
  sms: MyTwilioAdapter,
});

forRootAsync with ConfigModule

AuthModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    jwt: { secret: config.getOrThrow('JWT_SECRET') },
    cookie: { enabled: true, secure: config.get('NODE_ENV') === 'production' },
    session: { enabled: true },
  }),
});

useClass / useExisting factories implementing AuthOptionsFactory are also supported. When using forRootAsync, prefer passing adapter instances (not classes) so they are wired exactly as you construct them.


Adapters

Adapters are how the package talks to your infrastructure. Only UserAdapter is needed for stateful flows; the cache adapter defaults to in-memory.

| Token | Interface | Required for | | --- | --- | --- | | userAdapter | UserAdapter | credentials login, OAuth linking, 2FA persistence | | cache | CacheAdapter | sessions, OTP, magic links, OAuth state (Redis in prod) | | mail | MailAdapter | email OTP, magic links | | sms | SmsAdapter | SMS OTP | | inboundAdapters | InboundChannelAdapter[] | reverse OTP (inbound WhatsApp / Telegram) |

// Example: Prisma user adapter
import { Injectable } from '@nestjs/common';
import { UserAdapter, AuthUser, PasswordService } from '@elchinabilov/nestjs-auth';

@Injectable()
export class PrismaUserAdapter implements UserAdapter {
  constructor(private prisma: PrismaService, private passwords: PasswordService) {}

  async findById(id: string) {
    return this.prisma.user.findUnique({ where: { id } }) as Promise<AuthUser | null>;
  }

  async validateCredentials(email: string, password: string) {
    const user = await this.prisma.user.findUnique({ where: { email } });
    if (!user || !(await this.passwords.verify(password, user.passwordHash))) return null;
    return user as AuthUser;
  }

  async upsertOAuthUser(profile) {
    return this.prisma.user.upsert({
      where: { email: profile.email! },
      update: {},
      create: { email: profile.email!, name: profile.displayName },
    }) as Promise<AuthUser>;
  }
}

A RedisCacheAdapter is ~20 lines on top of ioredis implementing the CacheAdapter interface (get/set/del/incr/keys).


Authentication methods

1. Cookie-based authentication

Enable cookie options and pass the response to login — HttpOnly access & refresh cookies are set with your configured Secure/SameSite/domain. The AuthGuard reads them automatically.

await this.auth.login(user, { request: req, response: res }); // sets cookies
await this.auth.refresh(req.cookies.refresh_token, { response: res }); // rotates
await this.auth.logout({ sessionId: req.user.sessionId, response: res }); // clears

Refresh token rotation is built in (see sessions). CSRF: enable csrf and issue a token with CsrfService.issue(res); the guard validates the header against the cookie on state-changing requests.

2. Bearer / JWT authentication

@UseGuards(AuthGuard)
@Get('me')
me(@CurrentUser() user: AuthUser) { return user; }

Authorization: Bearer <token> is validated against the access-token secret. Access and refresh tokens may use separate secrets and lifetimes; the type claim prevents using one as the other. Restrict a route to specific strategies with @AuthMethods(AuthMethod.BEARER).

3. Session-based authentication

When session.enabled, login creates a server-side session (in your cache adapter) and embeds its id (sid) in the JWT. Every request re-validates the session, so you can revoke tokens server-side — true logout, not just an expiring JWT. Supports multi-device, rolling expiry, maxPerUser caps and refresh-token rotation with reuse detection (a replayed refresh token kills the session).

4. API key authentication

@UseGuards(ApiKeyGuard)            // x-api-key header
@Get('internal/metrics')
metrics() {}

@UseGuards(ApiKeyOrAuthGuard)      // accept a key OR a user token
@Get('data')
data() {}

Keys resolve to a principal carrying roles/permissions, so RBAC guards work for services too. Use a static keys map or an async validate callback.

5. OAuth authentication

@Public() @Get('google')
async google(@Res() res) {
  const { url } = await this.oauth.getAuthorizationUrl('google', {
    redirectUrl: '/dashboard',
  });
  res.redirect(url);
}

@Public() @Get('google/callback')
async callback(@Query('code') code, @Query('state') state, @Req() req, @Res({ passthrough: true }) res) {
  const { profile, redirectUrl } = await this.oauth.handleCallback('google', code, state);
  const user = await this.userAdapter.upsertOAuthUser(profile);
  return this.auth.login(user, { request: req, response: res });
}

Built-in Google and GitHub providers; the state nonce is stored in the cache for CSRF protection. Add any provider by implementing OAuthProvider and registering it (config under oauth.<name>, then oauthService.registerProvider).

6. OTP authentication

await this.otp.send({ channel: OtpChannel.EMAIL, destination: '[email protected]' });
await this.otp.verify('[email protected]', code, OtpPurpose.LOGIN); // throws on bad/expired

Numeric or alphanumeric codes, configurable length/TTL, per-code retry limit, resend cooldown, single-use, hashed at rest. Email works out of the box; SMS is an optional adapter (sms).

7. Reverse OTP authentication

Reverse (inbound) OTP flips the usual flow: instead of the server delivering a code, the server issues a code and the user sends it back from their own WhatsApp or Telegram. That inbound message proves the user controls the account/number. The package mints + verifies the code and tracks the session; it never sends anything outbound and stays provider-agnostic — provider logic lives entirely in your inbound adapters.

// 1. Create a verification session and show the code + target to the user.
//    e.g. "Send TELEGRAM the code 482913 to @my_login_bot"
const { sessionId, code } = await this.reverseOtp.createSession({
  channel: ReverseOtpChannel.TELEGRAM,
  identifier: pendingUserId,         // optional app-level binding
});

// 2. In your webhook route, hand the raw request to the service. The matching
//    adapter authenticates + normalizes it, then any code is matched.
@Public() @Post('webhooks/telegram')
async telegram(@Body() body, @Headers() headers) {
  await this.reverseOtp.handleInbound(ReverseOtpChannel.TELEGRAM, { body, headers });
  return { ok: true };
}

// 3. Poll (or react) on the session status to complete your login flow.
const status = await this.reverseOtp.getStatus(sessionId); // pending | approved | denied | expired
if (status === ReverseOtpStatus.APPROVED) {
  const session = await this.reverseOtp.getSession(sessionId);
  // session.verifiedSender → the proven phone number / Telegram user id
}

State lives entirely in the cache adapter (no database), codes are single-use and hashed at rest, and the feature is inert unless reverseOtp.enabled and an adapter for the channel is registered.

Telegram works out of the box via TelegramInboundAdapter, which normalizes the official Bot API Update payload and (optionally) verifies the X-Telegram-Bot-Api-Secret-Token header. WhatsApp is provider-agnostic: extend WhatsAppInboundAdapter for Twilio, Meta Cloud API, Infobip, 360dialog, etc.

// Provider-specific WhatsApp adapter (example: Twilio)
import { WhatsAppInboundAdapter, InboundWebhookRequest } from '@elchinabilov/nestjs-auth';

export class MyTwilioWhatsAppAdapter extends WhatsAppInboundAdapter {
  parse(req: InboundWebhookRequest) {
    const b = req.body as { From?: string; Body?: string; MessageSid?: string };
    if (!b?.From || !b?.Body) return [];
    return [this.message({
      from: b.From.replace(/^whatsapp:/, ''),
      text: b.Body,
      messageId: b.MessageSid,
      raw: b,
    })];
  }
  // optional: override verify(req) to check the X-Twilio-Signature header
}

Every adapter returns the same normalized InboundMessage (channel, from, text, messageId?, senderName?, timestamp?, raw?), so the core never depends on a provider. A webhook may yield several messages; handleInbound returns one result per message. Register adapters at startup via inboundAdapters, or at runtime with reverseOtp.registerAdapter(adapter). Custom code extraction from message text is configurable via reverseOtp.extractCodes.

8. Magic link authentication

await this.magicLink.sendToEmail('[email protected]', { redirectUrl: '/welcome' });
// on click:
const { identifier, redirectUrl } = await this.magicLink.verify(token);
const user = await this.userAdapter.findByEmail!(identifier);
await this.auth.login(user, { request: req, response: res });

Tokens are one-time-use, time-limited, hashed at rest, with optional redirect URL support.

9. Two-factor authentication

// enable
const e = this.twoFactor.generateEnrollment(user.email);
await this.userAdapter.saveTwoFactorSecret!(user.id, e.secret, e.hashedBackupCodes);
return { otpauthUrl: e.otpauthUrl, backupCodes: e.backupCodes }; // show once

// verify (Google Authenticator / Authy / 1Password compatible)
const ok = this.twoFactor.verifyToken(secret, code)
  || !!this.twoFactor.verifyBackupCode(code, hashedBackupCodes);

Protect sensitive routes with @RequireTwoFactor() + TwoFactorGuard. Mark a session 2FA-verified via sessionService.markTwoFactorVerified(sid) or by passing twoFactorVerified: true to auth.login.


Authorization

@UseGuards(AuthGuard, RolesGuard, PermissionsGuard)
@Roles('admin', 'editor')           // any of these roles
@Permissions('posts:write')         // all listed permissions
@Post('posts')
create() {}

@AnyPermission('posts:read', 'posts:write') // at least one
@Get('posts')
list() {}
  • @Roles(...)RolesGuard (RBAC, any-of)
  • @Permissions(...)PermissionsGuard (all-of), @AnyPermission(...) (any-of)
  • @Public() → bypass auth on a route even under a global guard
  • @CurrentUser() / @CurrentUser('id') → inject the user / a field
  • @AuthContext() → inject { user, sessionId, method, device, twoFactorVerified }

Register globally with APP_GUARD for secure-by-default:

{ provide: APP_GUARD, useClass: AuthGuard }

Device & session management

this.sessionService.listForUser(userId);            // active sessions (multi-device)
this.sessionService.revoke(sessionId);              // sign out one device
this.sessionService.revokeAllForUser(userId, keep); // sign out all (except current)
this.deviceService.extract(req);                    // { ip, userAgent, browser, os, deviceType, fingerprint }

IP and user-agent are tracked per session (best-effort parsing, no extra deps).

API reference

Services: AuthService, TokenService, PasswordService, CookieService, CsrfService, SessionService, ApiKeyService, OtpService, ReverseOtpService, MagicLinkService, TwoFactorService, DeviceService, OAuthService, AuthUserService, AuthConfigService.

Guards: AuthGuard, ApiKeyGuard, ApiKeyOrAuthGuard, RolesGuard, PermissionsGuard, TwoFactorGuard.

Decorators: @Public, @Roles, @Permissions, @AnyPermission, @CurrentUser, @AuthContext, @AuthMethods, @RequireTwoFactor.

All are exported from the package root with full TypeScript types.

Error handling

Failures throw typed exceptions carrying a stable machine code and the right HTTP status — never leaking secrets or stack traces:

InvalidCredentialsException, InvalidTokenException, TokenExpiredException, MissingTokenException, SessionExpiredException, InvalidApiKeyException, OtpException, ReverseOtpException, TwoFactorRequiredException, InvalidCsrfTokenException, InsufficientPermissionsException, AuthConfigurationError.

// example 401 body
{ "statusCode": 401, "message": "Token has expired", "code": "token_expired", "error": "Unauthorized" }

Testing

Every service is a plain injectable with explicit dependencies — trivial to unit-test. The package itself ships unit tests plus a DI-graph integration test:

npm test

License

MIT © Elchin Abilov