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