JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 72
  • Score
    100M100P100Q102333F
  • License MIT

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

Package Exports

  • @ambushsoftworks/nestjs-auth-graphql
  • @ambushsoftworks/nestjs-auth-graphql/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@ambushsoftworks/nestjs-auth-graphql) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

@ambushsoftworks/nestjs-auth-graphql

Production-grade authentication package for NestJS with GraphQL, extracted from the Lift fitness app.


🚨 BREAKING CHANGES in v0.2.0

Version 0.2.0 introduces a complete architectural refactor. If upgrading from v0.1.x, you MUST follow the migration guide.

Key Changes:

  • Package no longer exports concrete AuthResolver - you must create your own extending BaseAuthResolver<T>
  • All entities/DTOs are now interfaces (IAuthUser, IAuthSignupInput, etc.) - you define GraphQL types
  • Package owns canonical enums (AuthProvider, UserStatus) - consumers use these in code

Migration Time: 2-4 hours | Read: MIGRATION.md | Quick Start: QUICK-START.md

Why? v0.2.0 eliminates GraphQL schema conflicts, provides full control over types, and works with any database/ORM across diverse projects.


Features

  • JWT Authentication: Secure token-based authentication with automatic refresh
  • OAuth 2.0: Google and Facebook social login
  • Email Verification: 6-digit PIN codes via SendGrid with rate limiting
  • SMS Verification: Phone number verification via Twilio
  • Password Reset: 6-digit verification codes with email enumeration protection
  • Biometric Authentication: Face ID, Touch ID, fingerprint support
  • Brute Force Protection: Account lockout after failed login attempts
  • Account Linking: Link/unlink social accounts to existing accounts
  • Security: HMAC-SHA256 token hashing, AES-256-GCM encryption, constant-time comparison
  • Type Safety: Full TypeScript support with strict mode
  • Testing: 246+ tests from production codebase

Installation

npm install @yourorg/nestjs-auth-graphql

Peer Dependencies

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

Quick Start

1. Implement Required Repositories

The package uses interfaces for data persistence - you must implement these for your database (Prisma, TypeORM, etc.):

import { IUserRepository, IRefreshTokenRepository, IAuthUser } from '@yourorg/nestjs-auth-graphql';
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Injectable()
export class PrismaUserRepository implements IUserRepository {
  constructor(private prisma: PrismaService) {}

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

  async findByEmail(email: string): Promise<IAuthUser | null> {
    return this.prisma.user.findUnique({ where: { email } });
  }

  async create(data: any): Promise<IAuthUser> {
    return this.prisma.user.create({ data });
  }

  // Implement other required methods...
}

@Injectable()
export class PrismaRefreshTokenRepository implements IRefreshTokenRepository {
  constructor(private prisma: PrismaService) {}

  async create(data: any): Promise<any> {
    return this.prisma.refreshToken.create({ data });
  }

  // Implement other required methods...
}

2. Configure the Auth Module

IMPORTANT: This package uses instance-based dependency injection. You must register your repository/service classes in the providers array and inject instances into the useFactory function.

import { Module } from '@nestjs/common';
import { AuthModule } from '@yourorg/nestjs-auth-graphql';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PrismaModule } from './prisma/prisma.module';
import { PrismaUserRepository } from './repositories/prisma-user.repository';
import { PrismaRefreshTokenRepository } from './repositories/prisma-refresh-token.repository';

@Module({
  imports: [
    ConfigModule.forRoot(),
    PrismaModule,  // Import module that provides repositories
    AuthModule.forRootAsync({
      imports: [PrismaModule, ConfigModule],
      inject: [
        PrismaUserRepository,           // Inject repository instances
        PrismaRefreshTokenRepository,
        ConfigService,
      ],
      useFactory: (
        usersRepo: PrismaUserRepository,
        tokenRepo: PrismaRefreshTokenRepository,
        config: ConfigService,
      ) => ({
        // Pass instances directly (not classes)
        userRepositoryInstance: usersRepo,
        refreshTokenRepositoryInstance: tokenRepo,

        // JWT configuration (required)
        jwtSecret: config.get('JWT_SECRET'),
        jwtExpiresIn: '15m',  // Default: '15m'
        refreshTokenExpiresIn: '30d',  // Default: '30d'

        // Feature flags
        features: {
          emailVerification: true,
          smsVerification: true,
          googleOAuth: true,
          facebookOAuth: true,
          biometricAuth: true,
          bruteForceProtection: true,
        },

        // OAuth configuration
        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'),
          },
        },
      }),
    }),
  ],
  providers: [
    // Register repository classes so NestJS can inject them
    PrismaUserRepository,
    PrismaRefreshTokenRepository,
  ],
})
export class AppModule {}

Note: You must register your repository classes in the providers array of the module where you call forRootAsync(). NestJS will inject instances of these repositories into the factory function.

3. Create Your Auth Resolver

CRITICAL: Due to TypeScript decorator metadata limitations, you MUST create your own resolver extending BaseAuthResolver. The package provides business logic, but YOU define GraphQL types.

Create src/auth/auth.resolver.ts:

import { Resolver, Mutation, Args, Context, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import {
  BaseAuthResolver,
  JwtAuthGuard,
  CurrentUser
} from '@yourorg/nestjs-auth-graphql';
import { User } from '../users/entities/user.entity';  // Your User @ObjectType
import { AuthResponse } from './dto/auth-response.dto';  // Your AuthResponse @ObjectType
import { LoginInput } from './dto/login.input';  // Your @InputType classes
import { SignupInput } from './dto/signup.input';
import { RefreshTokenInput } from './dto/refresh-token.input';
import { LogoutInput } from './dto/logout.input';
import { LogoutResponse, LogoutAllResponse } from './dto/logout-response.dto';
// ... import other DTOs for email/SMS verification, etc.

/**
 * Your app's auth resolver - extends BaseAuthResolver for business logic
 */
@Resolver()
export class AuthResolver extends BaseAuthResolver<User> {
  // Core auth mutations (required - override to provide GraphQL types)

  @Mutation(() => AuthResponse)
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  async signup(@Args('input') input: SignupInput): Promise<AuthResponse> {
    return this.performSignup(input) as Promise<AuthResponse>;
  }

  @Mutation(() => AuthResponse)
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  async login(
    @Args('input') input: LoginInput,
    @Context() context: any,
  ): Promise<AuthResponse> {
    return this.performLogin(input, context) as Promise<AuthResponse>;
  }

  @Mutation(() => AuthResponse)
  @Throttle({ default: { limit: 10, ttl: 60000 } })
  async refreshToken(@Args('input') input: RefreshTokenInput): Promise<AuthResponse> {
    return this.performRefreshToken(input) as Promise<AuthResponse>;
  }

  @Mutation(() => LogoutResponse)
  @UseGuards(JwtAuthGuard)
  @Throttle({ default: { limit: 10, ttl: 60000 } })
  async logout(
    @Args('input') input: LogoutInput,
    @CurrentUser() user: User,
  ): Promise<LogoutResponse> {
    return this.performLogout(input, user) as Promise<LogoutResponse>;
  }

  @Mutation(() => LogoutAllResponse)
  @UseGuards(JwtAuthGuard)
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  async logoutAll(@CurrentUser() user: User): Promise<LogoutAllResponse> {
    return this.performLogoutAll(user) as Promise<LogoutAllResponse>;
  }

  // Add overrides for other mutations: verifyEmail, sendPhoneVerification, etc.
  // See full example in Lift backend: src/auth/lift-auth.resolver.ts
}

Why this pattern?

  • TypeScript decorator metadata is NOT preserved when importing classes from compiled npm packages
  • This causes "Cannot determine GraphQL output type" errors at runtime
  • Solution: Package provides protected perform*() methods, you add GraphQL decorators

Register your resolver:

@Module({
  imports: [AuthModule.forRootAsync(...)],
  providers: [
    AuthResolver,  // Add your resolver here
    PrismaUserRepository,
    PrismaRefreshTokenRepository,
  ],
})
export class AppModule {}

4. Configure Input Validation

IMPORTANT: This package includes input validation using class-validator decorators on all input DTOs. You must enable NestJS's ValidationPipe globally for automatic validation.

Add this to your main.ts:

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Enable validation globally
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,           // Strip properties not in DTO
      forbidNonWhitelisted: true, // Throw error if extra properties
      transform: true,            // Auto-transform payloads to DTO instances
      transformOptions: {
        enableImplicitConversion: true, // Auto-convert primitive types
      },
    }),
  );

  await app.listen(3000);
}
bootstrap();

What gets validated:

The package validates all input fields with appropriate decorators:

  • Email fields: @IsEmail() - Validates proper email format
  • Password fields (signup): @MinLength(8), @MaxLength(100), @Matches() - Enforces strong passwords with uppercase, lowercase, number, and special character
  • Password fields (login): @IsNotEmpty() - Validates presence only (no strength check for existing passwords)
  • Verification codes: @Matches(/^\d{6}$/) - Validates 6-digit PIN codes
  • Phone numbers: @Matches(/^\+[1-9]\d{1,14}$/) - Validates E.164 international format (e.g., +14155552671)
  • Tokens: @IsString(), @IsNotEmpty() - Validates refresh tokens and OAuth tokens
  • Device IDs: @IsUUID() or @IsString() - Validates biometric device identifiers
  • Provider names: @IsIn(['google', 'facebook', 'apple']) - Validates OAuth provider names

Validation error responses:

When validation fails, NestJS automatically returns a 400 Bad Request with detailed error messages:

{
  "statusCode": 400,
  "message": [
    "Please provide a valid email address",
    "Password must be at least 8 characters",
    "Password must contain uppercase, lowercase, number, and special character"
  ],
  "error": "Bad Request"
}

Custom validation for your DTOs:

If you create your own input DTOs implementing the package interfaces (recommended for full type control), add class-validator decorators:

import { InputType, Field } from '@nestjs/graphql';
import { IsEmail, IsString, MinLength, Matches } from 'class-validator';
import { IAuthSignupInput } from '@ambushsoftworks/nestjs-auth-graphql';

@InputType()
export class SignupInput implements IAuthSignupInput {
  @Field()
  @IsEmail({}, { message: 'Please provide a valid email address' })
  email: string;

  @Field()
  @IsString()
  @MinLength(12, { message: 'Password must be at least 12 characters' })
  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/, {
    message: 'Password must contain uppercase, lowercase, number, and special character',
  })
  password: string;
}

Note: The package's default input classes (marked as @deprecated) already include validation decorators for backward compatibility. If using these, validation works automatically with ValidationPipe enabled.

Configuration Options

Required Options

  • userRepositoryInstance: Instance of IUserRepository implementation
  • refreshTokenRepositoryInstance: Instance of IRefreshTokenRepository implementation
  • jwtSecret: Secret key for signing JWT tokens

Optional Options

  • emailServiceInstance: Instance of IEmailService implementation (default: null)
    • Use SendGridEmailService for production
    • Use NoOpEmailService for development/testing
  • smsServiceInstance: Instance of ISmsService implementation (default: null)
    • Use TwilioSmsService for production
    • Use NoOpSmsService for development/testing
  • lifecycleHooksInstance: Instance of IAuthLifecycleHooks implementation (default: null)
    • Add custom logic for signup, login, logout, etc.
  • verificationRepositoryInstance: Instance of IVerificationRepository implementation (default: NoOpVerificationRepository)
  • bruteForceRepositoryInstance: Instance of IBruteForceRepository implementation (default: NoOpBruteForceRepository)
  • biometricRepositoryInstance: Instance of IBiometricRepository implementation (default: NoOpBiometricRepository)
  • jwtExpiresIn: JWT access token expiration time (default: '15m')
  • refreshTokenExpiresIn: Refresh token expiration time (default: '30d')

Feature Flags

Control which authentication features are enabled:

features: {
  emailVerification: true,      // Email verification with PIN codes
  smsVerification: true,         // SMS verification with Twilio
  googleOAuth: true,             // Google Sign In
  facebookOAuth: true,           // Facebook Login
  biometricAuth: true,           // Face ID, Touch ID, fingerprint
  bruteForceProtection: true,    // Account lockout after failed attempts
}

Using Default Implementations

SendGrid Email Service

import { SendGridEmailService } from '@yourorg/nestjs-auth-graphql';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    AuthModule.forRootAsync({
      inject: [SendGridEmailService, ConfigService, /* ... */],
      useFactory: (emailSvc, config, /* ... */) => ({
        emailServiceInstance: emailSvc,
        // ...
      }),
    }),
  ],
  providers: [
    SendGridEmailService,  // Register in providers
  ],
})

// Environment variables required:
// SENDGRID_API_KEY=your_api_key
// SENDGRID_FROM_EMAIL=noreply@yourapp.com
// SENDGRID_FROM_NAME=Your App Name

Twilio SMS Service

import { TwilioSmsService } from '@yourorg/nestjs-auth-graphql';

@Module({
  imports: [
    AuthModule.forRootAsync({
      inject: [TwilioSmsService, /* ... */],
      useFactory: (smsSvc, /* ... */) => ({
        smsServiceInstance: smsSvc,
        // ...
      }),
    }),
  ],
  providers: [
    TwilioSmsService,  // Register in providers
  ],
})

// Environment variables required:
// TWILIO_ACCOUNT_SID=your_account_sid
// TWILIO_AUTH_TOKEN=your_auth_token
// TWILIO_PHONE_NUMBER=+1234567890

No-Op Services (Development/Testing)

import { NoOpEmailService, NoOpSmsService } from '@yourorg/nestjs-auth-graphql';

@Module({
  imports: [
    AuthModule.forRootAsync({
      inject: [NoOpEmailService, NoOpSmsService, /* ... */],
      useFactory: (emailSvc, smsSvc, /* ... */) => ({
        emailServiceInstance: emailSvc,
        smsServiceInstance: smsSvc,
        // ...
      }),
    }),
  ],
  providers: [
    NoOpEmailService,  // These log operations instead of sending actual emails/SMS
    NoOpSmsService,
  ],
})

Password Reset

Secure password reset flow with 6-digit verification codes, rate limiting, and email enumeration protection.

Features

  • 6-Digit Verification Codes - SMS/email verification pattern (not magic links)
  • Email Enumeration Protection - Generic success messages for all requests
  • Rate Limiting - 60-second cooldown between requests per user
  • Password Strength Validation - Configurable requirements (default: 8+ chars, uppercase, lowercase, number)
  • Token Revocation - All refresh tokens invalidated on password change
  • Brute Force Protection - Integration with account locking system
  • OAuth User Protection - Users authenticated via social login cannot reset passwords
  • Security Logging - Audit trail for all password reset activities

Consumer Setup

Step 1: Database Migration

Add passwordResetSentAt field to your User model:

Prisma Example:

model User {
  id                  String    @id @default(cuid())
  email               String    @unique
  passwordHash        String?

  // Password reset rate limiting
  passwordResetSentAt DateTime? // 60-second cooldown

  // ... other fields
}

TypeORM Example:

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ nullable: true })
  passwordResetSentAt: Date;

  // ... other fields
}

Step 2: Configure Email Service

Ensure SendGridEmailService (or your custom email service) is configured:

AuthModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService, UsersRepository, /* ... */],
  useFactory: (config: ConfigService, usersRepo, /* ... */) => ({
    // ... other options

    emailServiceInstance: new SendGridEmailService(
      config.get('SENDGRID_API_KEY'),
    ),

    // IMPORTANT: Set frontend URL for email templates
    // (Not used in 6-digit code flow, but required by email service)
    features: {
      emailVerification: true,
      // ... other features
    },
  }),
}),

Environment Variable:

FRONTEND_URL=https://yourapp.com  # Used in email branding

Step 3: Create GraphQL DTOs

Create consumer-specific DTOs with GraphQL decorators:

request-password-reset.input.ts:

import { InputType, Field } from '@nestjs/graphql';
import { IAuthRequestPasswordResetInput } from '@ambushsoftworks/nestjs-auth-graphql';

@InputType()
export class RequestPasswordResetInput implements IAuthRequestPasswordResetInput {
  @Field(() => String, {
    description: 'User email address. Code sent if account exists.',
  })
  email: string;
}

reset-password.input.ts:

import { InputType, Field } from '@nestjs/graphql';
import { IAuthResetPasswordInput } from '@ambushsoftworks/nestjs-auth-graphql';

@InputType()
export class ResetPasswordInput implements IAuthResetPasswordInput {
  @Field(() => String)
  email: string;

  @Field(() => String, { description: '6-digit verification code' })
  code: string;

  @Field(() => String, { description: 'New password (8+ chars, uppercase, lowercase, number)' })
  newPassword: string;
}

password-reset-response.dto.ts:

import { ObjectType, Field, Int } from '@nestjs/graphql';
import { IAuthPasswordResetResponse } from '@ambushsoftworks/nestjs-auth-graphql';

@ObjectType()
export class PasswordResetResponse implements IAuthPasswordResetResponse {
  @Field(() => Boolean)
  success: boolean;

  @Field(() => String)
  message: string;

  @Field(() => Int, { nullable: true })
  retryAfterSeconds?: number;
}

Step 4: Add Resolver Mutations

Extend your custom resolver with password reset mutations:

import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
import { Throttle } from '@nestjs/throttler';
import { BaseAuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
import { RequestPasswordResetInput } from './dto/request-password-reset.input';
import { ResetPasswordInput } from './dto/reset-password.input';
import { PasswordResetResponse } from './dto/password-reset-response.dto';
import { User } from './entities/user.entity';

@Resolver()
export class AppAuthResolver extends BaseAuthResolver<User> {
  // ... other mutations (signup, login, etc.)

  @Mutation(() => PasswordResetResponse, {
    name: 'requestPasswordReset',
    description: 'Request password reset code via email',
  })
  @Throttle({ default: { limit: 3, ttl: 60000 } }) // 3 requests per minute
  async requestPasswordReset(
    @Args('input') input: RequestPasswordResetInput,
    @Context() context: any,
  ): Promise<PasswordResetResponse> {
    return this.performRequestPasswordReset(input, context) as Promise<PasswordResetResponse>;
  }

  @Mutation(() => PasswordResetResponse, {
    name: 'resetPassword',
    description: 'Reset password using verification code',
  })
  @Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 attempts per 15 minutes
  async resetPassword(
    @Args('input') input: ResetPasswordInput,
    @Context() context: any,
  ): Promise<PasswordResetResponse> {
    return this.performResetPassword(input, context) as Promise<PasswordResetResponse>;
  }
}

Step 5: Deploy & Test

# Run migration
npx prisma migrate deploy

# Start dev server
npm run start:dev

# Test GraphQL API
curl -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { requestPasswordReset(input: {email: \"test@example.com\"}) { success message } }"}'

GraphQL Schema

After setup, your schema will include:

type Mutation {
  requestPasswordReset(input: RequestPasswordResetInput!): PasswordResetResponse!
  resetPassword(input: ResetPasswordInput!): PasswordResetResponse!
}

input RequestPasswordResetInput {
  email: String!
}

input ResetPasswordInput {
  email: String!
  code: String!
  newPassword: String!
}

type PasswordResetResponse {
  success: Boolean!
  message: String!
  retryAfterSeconds: Int
}

Error Handling

Expected Exceptions:

Exception HTTP Status When Thrown Client Action
PasswordResetRateLimitException 429 < 60 seconds since last request Display countdown: "Try again in X seconds"
WeakPasswordException 400 Password doesn't meet requirements Show validation errors to user
AccountLockedException 403 Account locked due to brute force Show "Account locked" message
UnauthorizedException 401 Invalid/expired code "Code is invalid or expired"

Example Client-Side Error Handling (GraphQL):

try {
  const result = await client.mutate({
    mutation: RESET_PASSWORD_MUTATION,
    variables: { input: { email, code, newPassword } },
  });

  if (result.data?.resetPassword?.success) {
    // Redirect to login
    router.push('/login');
  }
} catch (error) {
  if (error.extensions?.code === 'BAD_REQUEST') {
    // WeakPasswordException
    showErrors(error.extensions.errors); // ["Password must contain uppercase", ...]
  } else if (error.extensions?.code === 'TOO_MANY_REQUESTS') {
    // PasswordResetRateLimitException
    const retryAfter = error.extensions.retryAfterSeconds;
    showCountdown(retryAfter);
  } else if (error.message.includes('invalid or expired')) {
    // UnauthorizedException
    showError('Code is invalid or expired');
  }
}

Security Considerations

  1. Never reveal whether email exists

    • Always return success message, even for non-existent emails
    • Client cannot enumerate valid email addresses
  2. Rate limiting is essential

    • Implement both per-user (60s) AND per-IP (via @Throttle) limits
    • Prevents abuse and spam
  3. Code security

    • Codes are HMAC-SHA256 hashed in database (never plain text)
    • Constant-time comparison prevents timing attacks
    • 15-minute expiry limits exposure window
    • Single-use enforcement prevents replay attacks
  4. Token revocation

    • All refresh tokens are invalidated on password change
    • Forces re-authentication on all devices
    • Prevents attacker from maintaining access
  5. Email template security

    • Do NOT include personalized reset URLs with embedded tokens
    • Use 6-digit codes displayed in email (user manually enters in app)
    • Prevents phishing attacks via link manipulation

Lifecycle Hooks

Optionally track password reset events:

export class AppAuthHooks implements IAuthLifecycleHooks<User> {
  async onPasswordReset(user: User): Promise<void> {
    // Send security alert to user's phone
    await this.smsService.send(user.phoneNumber, 'Your password was just changed');

    // Log to analytics
    await this.analytics.track(user.id, 'password_reset_completed');

    // Revoke API keys (if your app has them)
    await this.apiKeyService.revokeAllKeys(user.id);
  }
}

GraphQL API

The package provides a complete GraphQL API:

Mutations

# Signup
mutation Signup($input: SignupInput!) {
  signup(signupInput: $input) {
    accessToken
    refreshToken
    user { id email }
  }
}

# Login
mutation Login($input: LoginInput!) {
  login(loginInput: $input) {
    accessToken
    refreshToken
    user { id email }
  }
}

# Refresh Token
mutation RefreshToken($input: RefreshTokenInput!) {
  refreshToken(refreshTokenInput: $input) {
    accessToken
    refreshToken
  }
}

# Logout
mutation Logout($input: LogoutInput!) {
  logout(logoutInput: $input) {
    success
  }
}

# Email Verification
mutation VerifyEmail($input: VerifyEmailInput!) {
  verifyEmail(verifyEmailInput: $input) {
    success
    user { id email emailVerified }
  }
}

# SMS Verification
mutation VerifyPhone($input: VerifyPhoneInput!) {
  verifyPhone(verifyPhoneInput: $input) {
    success
    user { id phoneNumber phoneVerified }
  }
}

# Password Reset Request
mutation RequestPasswordReset($input: RequestPasswordResetInput!) {
  requestPasswordReset(input: $input) {
    success
    message
    retryAfterSeconds
  }
}

# Password Reset Confirmation
mutation ResetPassword($input: ResetPasswordInput!) {
  resetPassword(input: $input) {
    success
    message
  }
}

# Google OAuth Account Linking
mutation LinkGoogleAccount($input: LinkGoogleAccountInput!) {
  linkGoogleAccount(linkGoogleAccountInput: $input) {
    user { id googleId }
  }
}

# Biometric Enrollment
mutation EnrollBiometric($input: EnrollBiometricInput!) {
  enrollBiometric(enrollBiometricInput: $input) {
    credentialId
    publicKey
  }
}

Queries

# Get current user
query Me {
  me {
    id
    email
    emailVerified
    phoneVerified
    googleId
    facebookId
  }
}

# Check account lock status
query CheckAccountLockStatus($email: String!) {
  checkAccountLockStatus(email: $email) {
    isLocked
    remainingLockoutTime
  }
}

# Get biometric auth status
query BiometricStatus {
  biometricStatus {
    isEnabled
    registeredDevices { deviceId deviceName }
  }
}

Implementing Custom Lifecycle Hooks

Add custom business logic to authentication events:

import { Injectable } from '@nestjs/common';
import { IAuthLifecycleHooks, IAuthUser } from '@yourorg/nestjs-auth-graphql';

@Injectable()
export class MyAuthHooks implements IAuthLifecycleHooks {
  async onSignup(user: IAuthUser): Promise<void> {
    // Send welcome email, create default settings, etc.
    console.log(`New user signed up: ${user.email}`);
  }

  async onLogin(user: IAuthUser): Promise<void> {
    // Update last login timestamp, log analytics, etc.
    console.log(`User logged in: ${user.email}`);
  }

  async onLogout(user: IAuthUser): Promise<void> {
    // Clean up sessions, log analytics, etc.
    console.log(`User logged out: ${user.email}`);
  }

  async onEmailVerified(user: IAuthUser): Promise<void> {
    // Send confirmation email, unlock features, etc.
    console.log(`Email verified: ${user.email}`);
  }

  async onPasswordReset(user: IAuthUser): Promise<void> {
    // Log security event, notify user, etc.
    console.log(`Password reset: ${user.email}`);
  }

  async onAccountLocked(user: IAuthUser, unlockTime: Date): Promise<void> {
    // Send notification, log security event, etc.
    console.log(`Account locked: ${user.email} until ${unlockTime}`);
  }

  async onSocialAccountLinked(user: IAuthUser, provider: string): Promise<void> {
    // Send confirmation email, log event, etc.
    console.log(`${provider} account linked: ${user.email}`);
  }

  async onSocialAccountUnlinked(user: IAuthUser, provider: string): Promise<void> {
    // Send confirmation email, log event, etc.
    console.log(`${provider} account unlinked: ${user.email}`);
  }
}

Security Features

Brute Force Protection

  • 5 failed login attempts → 15-minute account lockout
  • IP-based rate limiting: 10 attempts per minute
  • Custom exception with remainingLockoutTime field

Token Security

  • Access tokens: Short-lived (15 minutes default)
  • Refresh tokens: Long-lived (30 days), HMAC-SHA256 hashed
  • Token rotation: New refresh token issued on each refresh
  • Idempotent refresh: 10-second grace period prevents race conditions

Verification Codes

  • 6-digit PIN codes
  • HMAC-SHA256 hashing
  • 15-minute expiry
  • 3 max attempts per code
  • 60-second rate limit on resend

OAuth Security

  • Stateless CSRF protection: JWT-based state tokens
  • AES-256-GCM encryption: OAuth access tokens encrypted at rest
  • Account linking: Secure flow with linking tokens

Biometric Authentication

  • Public key cryptography: WebAuthn-compatible
  • Device enrollment: Multiple device support
  • Challenge-response: Prevents replay attacks

Security Best Practices

🔐 Secret Management

CRITICAL: Never commit secrets to version control. Use environment variables and secret management systems.

Required Secrets:

  1. JWT_SECRET:

    • Generate: openssl rand -base64 64
    • Minimum: 32 bytes (256 bits)
    • Rotate: Every 90 days or on suspected compromise
    • Store: Environment variables, AWS Secrets Manager, HashiCorp Vault, etc.
  2. ENCRYPTION_KEY (for OAuth):

    • Generate: openssl rand -hex 32 (produces 64-character hex string)
    • Required length: Exactly 32 bytes (64 hex characters)
    • Purpose: AES-256-GCM encryption for OAuth access tokens at rest
    • Auto-generated if not provided, but provide explicitly in production for consistency across instances
  3. OAuth Secrets:

    • Never expose in client-side code
    • Use environment-specific secrets (dev/staging/prod)
    • Rotate on suspected compromise

Example .env file (never commit this file):

# Generate with: openssl rand -base64 64
JWT_SECRET=your_random_64_byte_base64_secret

# Generate with: openssl rand -hex 32
ENCRYPTION_KEY=your_64_character_hex_string

# OAuth secrets from provider dashboards
GOOGLE_CLIENT_SECRET=...
FACEBOOK_CLIENT_SECRET=...

# Third-party API keys
SENDGRID_API_KEY=...
TWILIO_AUTH_TOKEN=...

Production Secret Management:

// ❌ BAD: Hard-coded secrets
AuthModule.forRootAsync({
  useFactory: () => ({
    jwtSecret: 'my-secret-key',  // NEVER DO THIS
  }),
});

// ✅ GOOD: Environment variables
AuthModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    jwtSecret: config.get<string>('JWT_SECRET'),  // Read from env
    encryptionKey: config.get<string>('ENCRYPTION_KEY'),
  }),
});

// ✅ BETTER: Validation with config module
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';

ConfigModule.forRoot({
  validationSchema: Joi.object({
    JWT_SECRET: Joi.string().min(32).required(),
    ENCRYPTION_KEY: Joi.string().length(64).pattern(/^[0-9a-f]{64}$/).required(),
    GOOGLE_CLIENT_SECRET: Joi.string().when('GOOGLE_CLIENT_ID', {
      is: Joi.exist(),
      then: Joi.required(),
    }),
  }),
});

🛡️ Token Configuration

Access Token Best Practices:

AuthModule.forRootAsync({
  useFactory: (config) => ({
    // Short-lived access tokens (minimize damage if compromised)
    jwtExpiresIn: '15m',  // Default: 15 minutes

    // For mobile apps with poor connectivity, consider 1h max
    // jwtExpiresIn: '1h',  // Longer for mobile, but less secure

    // NEVER use long-lived access tokens
    // jwtExpiresIn: '30d',  // ❌ INSECURE
  }),
});

Refresh Token Best Practices:

AuthModule.forRootAsync({
  useFactory: (config) => ({
    // Balance between security and user experience
    refreshTokenExpiresIn: '30d',  // Default: 30 days

    // For high-security applications, use shorter expiration
    // refreshTokenExpiresIn: '7d',  // Re-authenticate weekly

    // For consumer apps, longer is acceptable
    // refreshTokenExpiresIn: '90d',  // Re-authenticate quarterly
  }),
});

Token Rotation: This package automatically rotates refresh tokens on each use. Old tokens are invalidated to prevent replay attacks.

🔒 Password Policy

Enforce Strong Passwords:

The package includes default password validation (8+ characters, uppercase, lowercase, number, special character). For stronger policies:

import { InputType, Field } from '@nestjs/graphql';
import { IsEmail, IsString, MinLength, MaxLength, Matches } from 'class-validator';

@InputType()
export class SignupInput {
  @Field()
  @IsEmail({}, { message: 'Please provide a valid email address' })
  email: string;

  @Field()
  @IsString()
  @MinLength(12, { message: 'Password must be at least 12 characters' })
  @MaxLength(128, { message: 'Password must not exceed 128 characters' })
  @Matches(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/,
    { message: 'Password must contain uppercase, lowercase, number, and special character' }
  )
  password: string;
}

Additional Recommendations:

  • ✅ Check against common password lists (e.g., have-i-been-pwned API)
  • ✅ Implement password history (prevent reuse of last 5 passwords)
  • ✅ Require password change on first login (for admin-created accounts)
  • ✅ Implement password expiration (60-90 days for high-security apps)

⏱️ Rate Limiting

Configure Throttling:

import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    // Global rate limiting
    ThrottlerModule.forRoot([{
      ttl: 60000,  // 60 seconds
      limit: 100,  // 100 requests per minute
    }]),

    AuthModule.forRootAsync({
      // Auth-specific throttling is handled per-mutation
      // See AuthResolver for @Throttle() decorators
    }),
  ],
})

Per-Endpoint Throttling (already implemented in BaseAuthResolver):

  • Login: 5 requests/minute (prevent brute force)
  • Signup: 5 requests/minute (prevent spam)
  • Refresh: 10 requests/minute (allow frequent refreshes)
  • Verification codes: 3 requests/minute (prevent SMS/email bombing)

Brute Force Protection: Enabled by default - 5 failed login attempts = 15-minute account lockout.

🌐 HTTPS/TLS Requirements

PRODUCTION REQUIREMENT: All authentication endpoints MUST use HTTPS.

// Production enforcement example
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Enforce HTTPS in production
  if (process.env.NODE_ENV === 'production') {
    app.use((req, res, next) => {
      if (!req.secure && req.headers['x-forwarded-proto'] !== 'https') {
        return res.redirect(301, `https://${req.headers.host}${req.url}`);
      }
      next();
    });
  }

  await app.listen(3000);
}
bootstrap();

OAuth Callback URLs: Must be HTTPS in production (required by Google/Facebook).

📊 Security Logging

Implement Custom Security Logger for production monitoring:

import { Injectable } from '@nestjs/common';
import { IAuthLogger, SecurityEvent } from '@ambushsoftworks/nestjs-auth-graphql';

@Injectable()
export class ProductionAuthLogger implements IAuthLogger {
  constructor(
    private readonly datadogClient: DatadogClient,
    private readonly slackNotifier: SlackNotifier,
  ) {}

  log(event: SecurityEvent, metadata: Record<string, any>) {
    // Log to centralized logging service
    this.datadogClient.log({
      level: 'info',
      message: event,
      tags: ['auth', 'security'],
      ...metadata,
    });

    // Alert on suspicious events
    if (event === SecurityEvent.TOKEN_REUSE_DETECTED) {
      this.slackNotifier.send(`⚠️ Security Alert: Token reuse detected for user ${metadata.userId}`);
    }
  }

  error(message: string, trace?: string, context?: string) {
    this.datadogClient.error({ message, trace, context });
  }

  warn(message: string, context?: string) {
    this.datadogClient.warn({ message, context });
  }

  debug(message: string, context?: string) {
    // Only in development
    if (process.env.NODE_ENV === 'development') {
      this.datadogClient.debug({ message, context });
    }
  }

  verbose(message: string, context?: string) {
    // Only in development
    if (process.env.NODE_ENV === 'development') {
      this.datadogClient.log({ level: 'verbose', message, context });
    }
  }
}

Critical Events to Monitor:

  • TOKEN_REUSE_DETECTED: Possible security breach
  • ACCOUNT_LOCKED: High failed login attempts
  • LOGIN_FAILURE: Pattern analysis for attacks
  • VERIFICATION_CODE_FAILED: Potential brute force on codes

🚀 Production Deployment

Environment Separation:

# Development (.env.development)
NODE_ENV=development
JWT_SECRET=dev_secret_key
ENCRYPTION_KEY=dev_encryption_key

# Staging (.env.staging)
NODE_ENV=staging
JWT_SECRET=staging_secret_key_different_from_dev
ENCRYPTION_KEY=staging_encryption_key_different_from_dev

# Production (.env.production)
NODE_ENV=production
JWT_SECRET=production_secret_key_from_secrets_manager
ENCRYPTION_KEY=production_encryption_key_from_secrets_manager

Multi-Instance Deployment (Load Balanced):

  • See "Production Deployment" section in CLAUDE.md for cache limitations
  • Use Redis for shared refresh token cache across instances
  • OR configure sticky sessions on load balancer
  • OR accept 10-second grace period limitation for most apps

Security Checklist:

  • ✅ HTTPS enforced (no HTTP in production)
  • ✅ Secrets managed via environment variables or secret manager
  • ✅ CORS configured to allow only trusted domains
  • ✅ Rate limiting enabled globally
  • ✅ Security logging to centralized service
  • ✅ Database connections use TLS
  • ✅ OAuth callback URLs whitelisted
  • ✅ ValidationPipe enabled globally
  • ✅ Helmet middleware for HTTP security headers
  • ✅ CSRF protection enabled (built-in for OAuth)

Security Headers Example:

import helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Security headers
  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
      },
    },
    hsts: {
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true,
    },
  }));

  await app.listen(3000);
}

🔍 Security Audits

Regular Security Practices:

  1. Dependency Scanning: npm audit (weekly in CI/CD)
  2. Secret Scanning: Use tools like GitGuardian, TruffleHog
  3. Penetration Testing: Quarterly security assessments
  4. Log Review: Weekly review of security event logs
  5. Incident Response: Document and practice breach response procedures

Monitoring Alerts:

  • Failed login spike (>100/hour)
  • Account lockout spike (>10/hour)
  • Token reuse detection (any occurrence)
  • Unusual geographic login patterns
  • Multiple verification code failures

Database Schema Requirements

This package expects specific database tables to implement the repository interfaces. You have two options:

Create tables that directly match the package interfaces for optimal performance:

User Table

Required fields:

CREATE TABLE users (
  id VARCHAR(36) PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  passwordHash VARCHAR(255),  -- Nullable for OAuth-only accounts
  emailVerified BOOLEAN DEFAULT false,
  phoneNumber VARCHAR(20),    -- Nullable
  phoneVerified BOOLEAN DEFAULT false,
  googleId VARCHAR(255),      -- Nullable, unique
  facebookId VARCHAR(255),    -- Nullable, unique
  appleId VARCHAR(255),       -- Nullable, unique
  authProvider VARCHAR(20) DEFAULT 'local',  -- 'local' | 'google' | 'facebook' | 'apple'
  accountLockedUntil TIMESTAMP,  -- Nullable, for brute force protection
  lastLoginAt TIMESTAMP,
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

RefreshToken Table

CREATE TABLE refresh_tokens (
  id VARCHAR(36) PRIMARY KEY,
  userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token VARCHAR(255) NOT NULL,  -- Plain token (sent to client)
  hashedToken VARCHAR(255) NOT NULL UNIQUE,  -- HMAC-SHA256 hash for lookup
  expiresAt TIMESTAMP NOT NULL,
  deviceInfo TEXT,  -- Nullable, JSON with device/browser info
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_userId (userId),
  INDEX idx_hashedToken (hashedToken),
  INDEX idx_expiresAt (expiresAt)
);

EmailVerification Table

CREATE TABLE email_verifications (
  id VARCHAR(36) PRIMARY KEY,
  userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  codeHash VARCHAR(255) NOT NULL,  -- HMAC-SHA256 hash of 6-digit code
  expiresAt TIMESTAMP NOT NULL,
  attempts INT DEFAULT 0,
  used BOOLEAN DEFAULT false,
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_userId (userId),
  UNIQUE idx_userId_unused (userId, used)  -- Ensure only one active code per user
);

PhoneVerification Table

CREATE TABLE phone_verifications (
  id VARCHAR(36) PRIMARY KEY,
  userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  codeHash VARCHAR(255) NOT NULL,  -- HMAC-SHA256 hash of 6-digit code
  expiresAt TIMESTAMP NOT NULL,
  attempts INT DEFAULT 0,
  used BOOLEAN DEFAULT false,
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_userId (userId),
  UNIQUE idx_userId_unused (userId, used)
);

FailedLoginAttempt Table

CREATE TABLE failed_login_attempts (
  id VARCHAR(36) PRIMARY KEY,
  userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  ipAddress VARCHAR(45),  -- IPv4 or IPv6
  attemptedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_userId (userId),
  INDEX idx_attemptedAt (attemptedAt)
);

BiometricCredential Table

CREATE TABLE biometric_credentials (
  id VARCHAR(36) PRIMARY KEY,
  userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  credentialId VARCHAR(255) NOT NULL UNIQUE,  -- WebAuthn credential ID
  publicKey TEXT NOT NULL,  -- PEM-encoded ECDSA public key
  deviceName VARCHAR(255) NOT NULL,
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  lastUsedAt TIMESTAMP,
  INDEX idx_userId (userId),
  INDEX idx_credentialId (credentialId)
);

BiometricChallenge Table

CREATE TABLE biometric_challenges (
  id VARCHAR(36) PRIMARY KEY,
  userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  challenge VARCHAR(255) NOT NULL,  -- Base64-encoded random challenge
  expiresAt TIMESTAMP NOT NULL,
  used BOOLEAN DEFAULT false,
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_userId (userId),
  INDEX idx_expiresAt (expiresAt)
);

Option 2: Adapter Pattern (Flexible Schema)

If your database schema differs from the package expectations, create adapter repositories that translate between your schema and the package interfaces.

Example: Lift app uses a unified VerificationCode table for both email and SMS verification:

@Injectable()
export class PrismaVerificationRepository implements IVerificationRepository {
  constructor(private prisma: PrismaService) {}

  async storeVerificationCode(
    userId: string,
    code: string,
    expiresAt: Date,
    type: 'email' | 'phone',
  ): Promise<void> {
    // 1. Look up user to get email or phone (extra query)
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { email: true, phoneNumber: true },
    });

    const identifier = type === 'email' ? user.email : user.phoneNumber;

    // 2. Store in unified VerificationCode table
    await this.prisma.verificationCode.create({
      data: {
        type: type === 'email' ? 'email' : 'sms',
        identifier: identifier.toLowerCase(),
        codeHash: code,
        expiresAt,
        attempts: 0,
        used: false,
      },
    });
  }

  // Implement other methods with similar adapter logic...
}

Trade-offs:

  • Pro: No schema migration required, preserves existing logic
  • Con: Extra database queries (performance overhead)
  • Con: Adapter complexity increases maintenance burden

Recommendation: Use Option 1 for new projects. Use Option 2 for migrating existing apps with established schemas.

Environment Variables

# JWT
JWT_SECRET=your_secret_key_here

# SendGrid (if using SendGridEmailService)
SENDGRID_API_KEY=your_sendgrid_api_key
SENDGRID_FROM_EMAIL=noreply@yourapp.com
SENDGRID_FROM_NAME=Your App Name

# Twilio (if using TwilioSmsService)
TWILIO_ACCOUNT_SID=your_twilio_account_sid
TWILIO_AUTH_TOKEN=your_twilio_auth_token
TWILIO_PHONE_NUMBER=+1234567890

# Google OAuth (if using)
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CALLBACK_URL=https://yourapp.com/api/auth/google/callback

# Facebook OAuth (if using)
FACEBOOK_CLIENT_ID=your_facebook_app_id
FACEBOOK_CLIENT_SECRET=your_facebook_app_secret
FACEBOOK_CALLBACK_URL=https://yourapp.com/api/auth/facebook/callback

# Encryption (auto-generated if not provided)
ENCRYPTION_KEY=32_byte_hex_string

Testing

The package includes 246+ tests from the production Lift app:

npm test                # Run all tests
npm run test:watch      # Watch mode
npm run test:cov        # Coverage report

Architecture

This package follows Layer 0 architecture with complete interface abstraction:

  • No database coupling: All services use interfaces (IUserRepository, etc.)
  • No Prisma imports in package code
  • Dependency injection: Proper NestJS DI patterns
  • Type safety: Strict TypeScript mode enabled
  • Production tested: Extracted from Lift app with 246+ passing tests

Migration from Lift Codebase

If you're migrating from the Lift codebase:

  1. Replace import { AuthModule } from './auth/auth.module' with import { AuthModule } from '@yourorg/nestjs-auth-graphql'
  2. Implement IUserRepository and IRefreshTokenRepository using your Prisma models
  3. Configure AuthModule.forRootAsync() with your repositories and services
  4. Remove old src/auth/ directory (keep only repository implementations)

License

MIT

Support

For issues and questions, please open an issue on GitLab.