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 extendingBaseAuthResolver<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-graphqlPeer Dependencies
npm install @nestjs/common @nestjs/core @nestjs/config @nestjs/graphql @nestjs/jwt @nestjs/passport @nestjs/throttler graphql passport passport-jwt reflect-metadata rxjsQuick 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 ofIUserRepositoryimplementationrefreshTokenRepositoryInstance: Instance ofIRefreshTokenRepositoryimplementationjwtSecret: Secret key for signing JWT tokens
Optional Options
emailServiceInstance: Instance ofIEmailServiceimplementation (default: null)- Use
SendGridEmailServicefor production - Use
NoOpEmailServicefor development/testing
- Use
smsServiceInstance: Instance ofISmsServiceimplementation (default: null)- Use
TwilioSmsServicefor production - Use
NoOpSmsServicefor development/testing
- Use
lifecycleHooksInstance: Instance ofIAuthLifecycleHooksimplementation (default: null)- Add custom logic for signup, login, logout, etc.
verificationRepositoryInstance: Instance ofIVerificationRepositoryimplementation (default: NoOpVerificationRepository)bruteForceRepositoryInstance: Instance ofIBruteForceRepositoryimplementation (default: NoOpBruteForceRepository)biometricRepositoryInstance: Instance ofIBiometricRepositoryimplementation (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 NameTwilio 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=+1234567890No-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 brandingStep 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
Never reveal whether email exists
- Always return success message, even for non-existent emails
- Client cannot enumerate valid email addresses
Rate limiting is essential
- Implement both per-user (60s) AND per-IP (via @Throttle) limits
- Prevents abuse and spam
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
Token revocation
- All refresh tokens are invalidated on password change
- Forces re-authentication on all devices
- Prevents attacker from maintaining access
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
remainingLockoutTimefield
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:
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.
- Generate:
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
- Generate:
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-pwnedAPI) - ✅ 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 breachACCOUNT_LOCKED: High failed login attemptsLOGIN_FAILURE: Pattern analysis for attacksVERIFICATION_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_managerMulti-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:
- Dependency Scanning:
npm audit(weekly in CI/CD) - Secret Scanning: Use tools like GitGuardian, TruffleHog
- Penetration Testing: Quarterly security assessments
- Log Review: Weekly review of security event logs
- 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:
Option 1: Match the Package Schema (Recommended)
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_stringTesting
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 reportArchitecture
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:
- Replace
import { AuthModule } from './auth/auth.module'withimport { AuthModule } from '@yourorg/nestjs-auth-graphql' - Implement
IUserRepositoryandIRefreshTokenRepositoryusing your Prisma models - Configure
AuthModule.forRootAsync()with your repositories and services - Remove old
src/auth/directory (keep only repository implementations)
License
MIT
Support
For issues and questions, please open an issue on GitLab.