JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 72
  • Score
    100M100P100Q99908F
  • 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

@yourorg/nestjs-auth-graphql

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

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
  • 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. Use in Your Resolvers

The package provides a complete GraphQL resolver, but you can also use the services directly:

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { AuthService, CurrentUser, UseGuards, JwtAuthGuard } from '@yourorg/nestjs-auth-graphql';

@Resolver()
export class MyResolver {
  constructor(private authService: AuthService) {}

  @Query(() => String)
  @UseGuards(JwtAuthGuard)
  async whoami(@CurrentUser() user: IAuthUser) {
    return `You are ${user.email}`;
  }
}

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,
  ],
})

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 }
  }
}

# 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

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.