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

@ambushsoftworks/nestjs-auth-graphql

v0.6.7

Published

Production-grade authentication package for NestJS with GraphQL, supporting JWT, OAuth, email/SMS verification, and biometric auth

Readme

@ambushsoftworks/nestjs-auth-graphql

Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cookie auth, multi-tenancy, OAuth, email verification, and more -- with zero database coupling.

Table of Contents


Design Principles

  • Interface-driven persistence -- All storage is behind interfaces (IUserRepository, IRefreshTokenRepository, ITenantRepository, etc.). Bring your own database.
  • Instance-based DI -- Repositories and services are injected as instances via useFactory, not as classes.
  • Consumer owns GraphQL types -- The package provides BaseAuthResolver<T> with protected perform*() methods. You create your own resolver with @Mutation() decorators (see Why BaseAuthResolver?).
  • Guards are exported, not auto-registered -- You register guards in your own APP_GUARD chain to control execution order.
  • NoOp fallbacks -- Every optional dependency has a no-op implementation, so the module works with minimal config.

Installation

npm install @ambushsoftworks/nestjs-auth-graphql

Peer Dependencies

npm install @nestjs/common @nestjs/core @nestjs/graphql @nestjs/jwt @nestjs/passport @nestjs/throttler graphql passport passport-jwt reflect-metadata rxjs

resend is an optional peer dependency -- install it only if using the Resend email sender.

Quick Start

Minimal setup: JWT authentication with email/password login.

1. Implement the required repositories

// users.repository.ts
import { Injectable } from '@nestjs/common';
import { IUserRepositoryCore, CreateUserData } from '@ambushsoftworks/nestjs-auth-graphql';

@Injectable()
export class UsersRepository implements IUserRepositoryCore<User> {
  async findByEmail(email: string): Promise<User | null> { /* ... */ }
  async findById(id: string): Promise<User | null> { /* ... */ }
  async create(data: CreateUserData): Promise<User> { /* ... */ }
  // ... implement remaining IUserRepositoryCore methods
}
// refresh-token.repository.ts
import { Injectable } from '@nestjs/common';
import { IRefreshTokenRepository } from '@ambushsoftworks/nestjs-auth-graphql';

@Injectable()
export class RefreshTokenRepository implements IRefreshTokenRepository {
  async create(data: {
    userId: string;
    token: string;
    hashedToken: string;
    expiresAt: Date;
    deviceInfo?: string | null;
  }): Promise<any> { /* ... */ }
  async findByHashedToken(hashedToken: string): Promise<any | null> { /* ... */ }
  async deleteByUserId(userId: string): Promise<number> { /* ... */ }
  // ... implement remaining methods
}

2. Register the module

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuthModule } from '@ambushsoftworks/nestjs-auth-graphql';

@Module({
  imports: [
    AuthModule.forRootAsync({
      imports: [ConfigModule, DatabaseModule],
      inject: [UsersRepository, RefreshTokenRepository, ConfigService],
      useFactory: (usersRepo, tokenRepo, config) => ({
        userRepositoryInstance: usersRepo,
        refreshTokenRepositoryInstance: tokenRepo,
        jwtSecret: config.get('JWT_SECRET'),
      }),
    }),
  ],
  providers: [UsersRepository, RefreshTokenRepository],
})
export class AppModule {}

3. Create your auth resolver

import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
import { Inject } from '@nestjs/common';
import {
  BaseAuthResolver,
  CurrentUser,
  AuthService,
  BruteForceProtectionService,
  USER_REPOSITORY,
  AUTH_LOGGER,
} from '@ambushsoftworks/nestjs-auth-graphql';

@Resolver()
export class AuthResolver extends BaseAuthResolver<User> {
  constructor(
    authService: AuthService,
    bruteForceProtection: BruteForceProtectionService,
    @Inject(USER_REPOSITORY) userRepository: any,
    @Inject(AUTH_LOGGER) authLogger: any,
  ) {
    super(authService, bruteForceProtection, userRepository, authLogger);
  }

  @Mutation(() => AuthResponse)
  async signup(@Args('input') input: SignupInput, @Context() ctx: any) {
    return this.performSignup(input, ctx);
  }

  @Mutation(() => AuthResponse)
  async login(@Args('input') input: LoginInput, @Context() ctx: any) {
    return this.performLogin(input, ctx);
  }

  @Mutation(() => AuthResponse)
  async refreshToken(@Args('input') input: RefreshTokenInput, @Context() ctx: any) {
    return this.performRefreshToken(input, ctx);
  }

  @Mutation(() => LogoutResponse)
  async logout(@Args('input') input: LogoutInput, @CurrentUser() user: User) {
    return this.performLogout(input, user);
  }
}

4. Set up the guard chain

import { APP_GUARD } from '@nestjs/core';
import { createAuthGuard } from '@ambushsoftworks/nestjs-auth-graphql';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: createAuthGuard(['jwt'], { allowPublic: true }),
    },
  ],
})
export class AppModule {}

Mark public routes with @Public():

import { Public } from '@ambushsoftworks/nestjs-auth-graphql';

@Public()
@Mutation(() => AuthResponse)
async login() { /* ... */ }

Why BaseAuthResolver?

TypeScript decorator metadata is not preserved when importing from compiled npm packages. If the package exported a resolver with @Mutation(() => AuthResponse), NestJS would throw "Cannot determine GraphQL output type" at runtime.

BaseAuthResolver<T> solves this by providing only business logic via protected perform*() methods. You add the GraphQL decorators in your own code, where metadata resolution works correctly.

When cookie auth is enabled, pass the GraphQL context so tokens are set as HttpOnly cookies. The response body will contain empty strings for accessToken/refreshToken.

Available perform*() methods

Authentication:

| Method | Purpose | |--------|---------| | performSignup(input, context?) | Create account | | performLogin(input, context) | Authenticate | | performRefreshToken(input, context?) | Rotate tokens | | performLogout(input, user, context?) | Invalidate token | | performLogoutAll(user, context?) | Invalidate all tokens | | performGetCurrentUser(userId) | Get current user |

Verification:

| Method | Purpose | |--------|---------| | performVerifyEmail(input, context?) | Email verification | | performResendVerificationEmail(email) | Resend verification email | | performSendPhoneVerification(input, user) | Send SMS verification code | | performVerifyPhone(input, user) | Verify phone with SMS code | | performResendPhoneVerification(phoneNumber, user) | Resend phone verification | | performRemovePhoneNumber(user) | Remove phone number from account | | performPhoneVerificationStatus(user) | Get phone verification status |

Password:

| Method | Purpose | |--------|---------| | performRequestPasswordReset(input, context?) | Send reset code/token | | performResetPassword(input, context?) | Reset password | | performChangePassword(userId, current, new) | Change password |

Other:

| Method | Purpose | |--------|---------| | performCheckAccountLockStatus(email) | Check brute force lock status | | performCompleteFacebookSignUp(input) | Facebook email fallback |


Features

Cookie Authentication

Enable features.cookieAuth to deliver tokens via HttpOnly cookies instead of response bodies.

AuthModule.forRootAsync({
  useFactory: () => ({
    // ...required options
    features: { cookieAuth: true },
    cookie: {
      httpOnly: true,     // default
      secure: true,       // default
      sameSite: 'lax',    // default
      domain: undefined,  // browser uses request domain
      useHostPrefix: true, // prefix names with __Host- for enhanced security
    },
  }),
})

When useHostPrefix: true, cookie names become __Host-access_token and __Host-refresh_token. The module validates at startup that secure is true, path is /, and domain is unset (as required by the __Host- spec).

You can also customize cookie names and max ages -- see Cookie Options.

The JWT strategy automatically reads from cookies first, then falls back to Authorization: Bearer headers.

CSRF Protection

With cookie auth, register CsrfGuard to protect mutations. It validates that a configurable header (default: X-Requested-With) is present when the request is authenticated via cookies.

import { APP_GUARD } from '@nestjs/core';
import { createAuthGuard, CsrfGuard } from '@ambushsoftworks/nestjs-auth-graphql';

@Module({
  providers: [
    { provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) },
    { provide: APP_GUARD, useClass: CsrfGuard },
  ],
})

Configure via csrf options:

csrf: {
  headerName: 'X-Requested-With',  // default
  requireInProduction: true,        // default
  exemptOperations: ['refreshToken'], // skip CSRF for specific operations
}

The guard automatically skips when:

  • The request uses Authorization header (Bearer/API key)
  • The route has @Public()
  • Cookie auth is not enabled
  • requireInProduction is false and not in production
  • The GraphQL operation is in exemptOperations

Multi-Tenancy

Enable tenant resolution and permission checking across requests.

AuthModule.forRootAsync({
  useFactory: (tenantRepo, tenantExtractor) => ({
    // ...required options
    features: { multiTenancy: true },
    tenantRepositoryInstance: tenantRepo,
    tenantExtractorInstance: tenantExtractor,
  }),
})

Guard chain (order matters):

providers: [
  { provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) },
  { provide: APP_GUARD, useClass: TenantGuard },
  { provide: APP_GUARD, useClass: PermissionGuard },
]

ITenantRepository resolves whether a user has access to a tenant:

@Injectable()
class TenantRepository implements ITenantRepository {
  async resolveTenant(userId: string, tenantId: string): Promise<ITenantContext | null> {
    const membership = await this.db.membership.find({ userId, tenantId });
    if (!membership) return null;
    return {
      tenantId,
      permissions: membership.role.permissions,
      metadata: { accountId: membership.accountId },
    };
  }
}

ITenantExtractor pulls the tenant ID from the request. Use the built-in HeaderTenantExtractor (reads x-tenant-id header) or implement your own:

import { HeaderTenantExtractor } from '@ambushsoftworks/nestjs-auth-graphql';

// Default: reads x-tenant-id header
const extractor = new HeaderTenantExtractor();

// Custom header name
const extractor = new HeaderTenantExtractor('x-org-id');

Access tenant context in resolvers:

@Query(() => [Item])
async items(@CurrentTenant() tenant: ITenantContext) {
  return this.service.findByTenant(tenant.tenantId);
}

Skip tenant resolution for routes that don't need it:

@SkipTenant()
@Query(() => User)
async me(@CurrentUser() user: User) { /* ... */ }

Permission checking with @RequirePermissions():

@RequirePermissions('clients:read')
@Query(() => [Client])
async clients(@CurrentTenant() tenant: ITenantContext) { /* ... */ }

For resource-scoped permissions, combine with @ResourceScope() and provide an IResourcePermissionRepository:

@RequirePermissions('clients:write')
@ResourceScope('client', 'clientId')
@Mutation(() => Client)
async updateClient(@Args('clientId') clientId: string) { /* ... */ }

API Key Authentication

For machine-to-machine auth, provide an IApiKeyRepository:

AuthModule.forRootAsync({
  useFactory: (apiKeyRepo) => ({
    // ...required options
    apiKeyRepositoryInstance: apiKeyRepo,
  }),
})

Then use a multi-strategy guard:

{ provide: APP_GUARD, useClass: createAuthGuard(['api-key', 'jwt'], { allowPublic: true }) }

The ApiKeyStrategy hashes the bearer token with SHA-256 and calls findByKeyHash() on your repository. API keys and JWT tokens use the same Authorization: Bearer header -- strategies are tried in the order specified. When no bearer token is present (e.g. cookie auth), the API key strategy gracefully falls through to the next strategy.

Email System

The email system uses a composable architecture: a sender (transport) and a template renderer (HTML generation).

import { SendGridEmailSender } from '@ambushsoftworks/nestjs-auth-graphql';

AuthModule.forRootAsync({
  useFactory: (config) => ({
    // ...required options
    email: {
      sender: new SendGridEmailSender(config.get('SENDGRID_API_KEY')),
      from: { email: '[email protected]', name: 'MyApp' },
      branding: {
        appName: 'MyApp',
        primaryColor: '#1976D2',
        logoUrl: 'https://example.com/logo.png',
        companyName: 'My Company',
        supportEmail: '[email protected]',
      },
    },
  }),
})

email.branding is required when using the default template renderer. If you provide a custom email.templateRenderer, branding can be omitted.

Built-in senders: SendGridEmailSender, ResendEmailSender, NoOpEmailSender

Custom sender: Implement IEmailSender:

class SmtpEmailSender implements IEmailSender {
  async send(params: {
    to: string;
    from: { email: string; name?: string };
    subject: string;
    html: string;
    text?: string;
  }) {
    // your SMTP logic
  }
}

Custom template renderer: Override DefaultEmailTemplateRenderer by providing email.templateRenderer:

email: {
  sender: new SendGridEmailSender(apiKey),
  from: { email: '[email protected]' },
  templateRenderer: new MyCustomRenderer(),
}

Verification Modes

Two modes for email verification and password reset:

  • 'code' (default) -- 6-digit numeric codes
  • 'token' -- URL-based tokens with configurable base URL
features: { verificationMode: 'token' },
verification: {
  baseUrl: 'https://app.example.com',
  tokenLength: 64,          // bytes, default: 64
  tokenExpiresInMinutes: 60, // default: 60
},

OAuth

Google and Facebook OAuth with encrypted token storage (AES-256-GCM).

AuthModule.forRootAsync({
  useFactory: (config) => ({
    // ...required options
    encryptionKey: config.get('ENCRYPTION_KEY'), // 32-byte hex string
    oauth: {
      google: {
        clientId: config.get('GOOGLE_CLIENT_ID'),
        clientSecret: config.get('GOOGLE_CLIENT_SECRET'),
        callbackUrl: config.get('GOOGLE_CALLBACK_URL'),
      },
      facebook: {
        clientId: config.get('FACEBOOK_CLIENT_ID'),
        clientSecret: config.get('FACEBOOK_CLIENT_SECRET'),
        callbackUrl: config.get('FACEBOOK_CALLBACK_URL'),
      },
    },
  }),
})

The OAuthController is automatically registered and handles OAuth callback routes. OAuth strategies gracefully degrade to no-ops when credentials are not configured.

Brute Force Protection

When features.bruteForceProtection is enabled and a bruteForceRepositoryInstance is provided, the module tracks failed login attempts and temporarily locks accounts. The lockout policy (attempt thresholds, lockout duration) is determined by your IBruteForceRepository implementation.

Password Policy

passwordPolicy: {
  minLength: 12,            // default: 8
  maxLength: 72,            // bcrypt limit
  requireUppercase: true,   // default: true
  requireLowercase: true,   // default: true
  requireNumber: true,      // default: true
  requireSpecialChar: true, // default: false
  customValidator: async (password) => {
    // Optional: dictionary checks, leaked password detection, etc.
    const isCommon = await checkLeakedPasswords(password);
    return {
      isValid: !isCommon,
      errors: isCommon ? ['Password found in data breaches'] : [],
    };
  },
}

Lifecycle Hooks

React to auth events without modifying core logic:

lifecycleHooksInstance: {
  async onSignup(user) { /* create default settings, send analytics */ },
  async onLogin(user) { /* update metrics */ },
  async onLogout(user) { /* cleanup */ },
  async onEmailVerified(user) { /* unlock features */ },
  async onPhoneVerified(user) { /* enable 2FA */ },
  async onOAuthAccountLinked(user, provider) { /* sync data */ },
  async onPasswordReset(user) { /* send security alert */ },
  async onPasswordChanged(user) { /* notify user */ },
  async onAccountDelete(user) { /* cleanup user data */ },
  onAuthFailure(email, ip, reason) { /* security monitoring */ },
}

All hooks are optional. Async hooks are awaited but failures are logged and do not block the auth operation. onAuthFailure is synchronous (fire-and-forget).

JWT Validation Modes

  • 'full' (default) -- Calls userRepository.findById() on every request to verify the user still exists.
  • 'payload-only' -- Returns { id, email } from the JWT payload without a DB lookup. Faster, but does not detect deleted/disabled users until token expiry.
jwtValidation: 'payload-only',

JWT Payload Factory

Customize JWT claims by providing an IJwtPayloadFactory:

jwtPayloadFactoryInstance: {
  createPayload(user) {
    return { sub: user.id, email: user.email, role: user.role };
  },
  extractUserId(payload) {
    return payload.sub;
  },
}

Configuration Reference

Required Options

| Option | Type | Description | |--------|------|-------------| | userRepositoryInstance | IUserRepository | User persistence | | refreshTokenRepositoryInstance | IRefreshTokenRepository | Token persistence | | jwtSecret | string | JWT signing secret |

Common Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | jwtExpiresIn | string | '15m' | Access token TTL | | refreshTokenExpiresIn | string | '30d' | Refresh token TTL | | jwtValidation | 'full' \| 'payload-only' | 'full' | JWT validation strategy | | bcryptRounds | number | 12 | Password hashing cost | | nodeEnv | string | 'production' | Environment identifier | | passwordPolicy | PasswordPolicyConfig | See Password Policy | Password strength rules | | encryptionKey | string | -- | 32-byte hex string for AES-256-GCM (OAuth token encryption) |

Feature Flags (features)

| Flag | Default | Description | |------|---------|-------------| | cookieAuth | false | Token delivery via HttpOnly cookies | | multiTenancy | false | Enable TenantGuard + PermissionGuard | | bruteForceProtection | false | Account lockout on failed logins | | preventEnumerationOnSignup | false | Return synthetic success for duplicate emails | | verificationMode | 'code' | 'code' for 6-digit codes, 'token' for URL tokens | | emailVerification | false | Enable email verification flow | | smsVerification | false | Enable SMS verification flow | | googleOAuth | false | Enable Google OAuth | | facebookOAuth | false | Enable Facebook OAuth | | biometricAuth | false | Enable biometric authentication |

Security Secrets

Separate HMAC secrets for security isolation. All fall back to jwtSecret if not provided.

| Option | Type | Description | |--------|------|-------------| | refreshTokenSecret | string | HMAC secret for refresh token hashing | | verificationCodeSecret | string | HMAC secret for verification code hashing | | oauthStateSecret | string | JWT secret for OAuth CSRF state tokens | | oauthRedirectWhitelist | string | Comma-separated allowed OAuth redirect URLs |

Cookie Options (cookie)

| Option | Type | Default | Description | |--------|------|---------|-------------| | httpOnly | boolean | true | HttpOnly flag | | secure | boolean | true | Secure flag | | sameSite | 'lax' \| 'strict' \| 'none' | 'lax' | SameSite attribute | | domain | string | undefined | Cookie domain | | path | string | '/' | Cookie path | | accessTokenName | string | 'access_token' | Access token cookie name | | refreshTokenName | string | 'refresh_token' | Refresh token cookie name | | accessTokenMaxAge | number | derived from jwtExpiresIn | Access token cookie max age (ms) | | refreshTokenMaxAge | number | derived from refreshTokenExpiresIn | Refresh token cookie max age (ms) | | useHostPrefix | boolean | false | Prefix names with __Host- |

CSRF Options (csrf)

| Option | Type | Default | Description | |--------|------|---------|-------------| | headerName | string | 'X-Requested-With' | Header to check | | requireInProduction | boolean | true | Require CSRF header in production | | exemptOperations | string[] | [] | GraphQL operations to skip |

Composable Email (email)

| Option | Type | Description | |--------|------|-------------| | email.sender | IEmailSender | Transport (SendGrid, Resend, etc.) -- required | | email.from | { email, name? } | Sender address -- required | | email.branding | EmailBrandingConfig | App name, colors, logo, etc. -- required unless templateRenderer is provided | | email.templateRenderer | IEmailTemplateRenderer | Override default HTML renderer |

When email is provided, it takes precedence over emailServiceInstance.

Verification Options (verification)

| Option | Type | Default | Description | |--------|------|---------|-------------| | tokenLength | number | 64 | Token length in bytes (token mode) | | tokenExpiresInMinutes | number | 60 | Token expiration (token mode) | | baseUrl | string | -- | Base URL for verification/reset URLs |

Optional Instance Options

| Option | Interface | Fallback | |--------|-----------|----------| | emailServiceInstance | IEmailService | NoOpEmailService | | smsServiceInstance | ISmsService | NoOpSmsService | | lifecycleHooksInstance | IAuthLifecycleHooks | {} (no-op) | | verificationRepositoryInstance | IVerificationRepository | NoOpVerificationRepository | | bruteForceRepositoryInstance | IBruteForceRepository | NoOpBruteForceRepository | | biometricRepositoryInstance | IBiometricRepository | NoOpBiometricRepository | | authLoggerInstance | IAuthLogger | ConsoleAuthLogger | | tenantRepositoryInstance | ITenantRepository | null | | tenantExtractorInstance | ITenantExtractor | null | | resourcePermissionRepositoryInstance | IResourcePermissionRepository | null | | jwtPayloadFactoryInstance | IJwtPayloadFactory | DefaultJwtPayloadFactory | | apiKeyRepositoryInstance | IApiKeyRepository | null | | rateLimiterInstance | IRateLimiter | InMemoryRateLimiterService |


Decorators

| Decorator | Target | Description | |-----------|--------|-------------| | @Public() | Method/Class | Skip authentication | | @PublicEndpoint() | Method/Class | Skip authentication + tenant resolution (combines @Public() + @SkipTenant()) | | @SkipTenant() | Method/Class | Skip tenant resolution | | @RequirePermissions('p1', 'p2') | Method/Class | Require specific permissions | | @CurrentUser() | Parameter | Inject authenticated user | | @CurrentTenant() | Parameter | Inject resolved tenant context | | @ResourceScope('resourceType', 'argName') | Method | Resource-level permission scoping |

Guards

| Guard | Description | |-------|-------------| | createAuthGuard(strategies, options) | Factory for multi-strategy auth guards | | CsrfGuard | CSRF header validation for cookie auth | | TenantGuard | Multi-tenant context resolution | | PermissionGuard | Permission checking against tenant context | | JwtAuthGuard | Basic JWT guard (prefer createAuthGuard) |

Recommended Guard Chains

// JWT only
{ provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) }

// JWT + API keys
{ provide: APP_GUARD, useClass: createAuthGuard(['jwt', 'api-key'], { allowPublic: true }) }

// Full stack with cookie auth + multi-tenancy
{ provide: APP_GUARD, useClass: CsrfGuard },
{ provide: APP_GUARD, useClass: createAuthGuard(['jwt', 'api-key'], { allowPublic: true }) },
{ provide: APP_GUARD, useClass: TenantGuard },
{ provide: APP_GUARD, useClass: PermissionGuard },

Utilities

Helper functions exported for use in custom guards and middleware:

| Function | Description | |----------|-------------| | getRequestFromContext(context) | Extract request from ExecutionContext (handles GraphQL + HTTP) | | extractIpAddress(request) | Extract client IP (checks clientIp, req.ip, falls back to 'unknown') |

import { getRequestFromContext, extractIpAddress } from '@ambushsoftworks/nestjs-auth-graphql';

@Injectable()
export class CustomGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = getRequestFromContext(context);
    const ip = extractIpAddress(request);
    // your logic
    return true;
  }
}

Security Features

  • Refresh token rotation with HMAC-SHA256 hashing and idempotent 10-second grace period
  • Bcrypt password hashing with configurable rounds (default: 12)
  • AES-256-GCM encryption for OAuth tokens at rest
  • Constant-time comparison for verification codes
  • Account enumeration prevention on signup (optional)
  • Stateless CSRF protection for OAuth flows via OAuthStateService
  • __Host- cookie prefix support for enhanced cookie security
  • Separate HMAC secrets for refresh tokens, verification codes, and OAuth state

License

MIT -- see LICENSE.