@ambushsoftworks/nestjs-auth-graphql 0.4.0 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +528 -1313
- package/dist/auth.module.d.ts +53 -2
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js +115 -16
- package/dist/auth.module.js.map +1 -1
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -1
- package/dist/constants.js.map +1 -1
- package/dist/decorators/current-tenant.decorator.d.ts +2 -0
- package/dist/decorators/current-tenant.decorator.d.ts.map +1 -0
- package/dist/decorators/current-tenant.decorator.js +10 -0
- package/dist/decorators/current-tenant.decorator.js.map +1 -0
- package/dist/decorators/public-endpoint.decorator.d.ts +2 -0
- package/dist/decorators/public-endpoint.decorator.d.ts.map +1 -0
- package/dist/decorators/public-endpoint.decorator.js +9 -0
- package/dist/decorators/public-endpoint.decorator.js.map +1 -0
- package/dist/decorators/public.decorator.d.ts +3 -0
- package/dist/decorators/public.decorator.d.ts.map +1 -0
- package/dist/decorators/public.decorator.js +8 -0
- package/dist/decorators/public.decorator.js.map +1 -0
- package/dist/decorators/require-permissions.decorator.d.ts +2 -0
- package/dist/decorators/require-permissions.decorator.d.ts.map +1 -0
- package/dist/decorators/require-permissions.decorator.js +8 -0
- package/dist/decorators/require-permissions.decorator.js.map +1 -0
- package/dist/decorators/resource-scope.decorator.d.ts +2 -0
- package/dist/decorators/resource-scope.decorator.d.ts.map +1 -0
- package/dist/decorators/resource-scope.decorator.js +8 -0
- package/dist/decorators/resource-scope.decorator.js.map +1 -0
- package/dist/decorators/skip-tenant.decorator.d.ts +2 -0
- package/dist/decorators/skip-tenant.decorator.d.ts.map +1 -0
- package/dist/decorators/skip-tenant.decorator.js +8 -0
- package/dist/decorators/skip-tenant.decorator.js.map +1 -0
- package/dist/guards/create-auth-guard.d.ts +11 -0
- package/dist/guards/create-auth-guard.d.ts.map +1 -0
- package/dist/guards/create-auth-guard.js +49 -0
- package/dist/guards/create-auth-guard.js.map +1 -0
- package/dist/guards/csrf.guard.d.ts +16 -0
- package/dist/guards/csrf.guard.d.ts.map +1 -0
- package/dist/guards/csrf.guard.js +90 -0
- package/dist/guards/csrf.guard.js.map +1 -0
- package/dist/guards/permission.guard.d.ts +12 -0
- package/dist/guards/permission.guard.d.ts.map +1 -0
- package/dist/guards/permission.guard.js +90 -0
- package/dist/guards/permission.guard.js.map +1 -0
- package/dist/guards/tenant.guard.d.ts +13 -0
- package/dist/guards/tenant.guard.d.ts.map +1 -0
- package/dist/guards/tenant.guard.js +85 -0
- package/dist/guards/tenant.guard.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/api-key-repository.interface.d.ts +11 -0
- package/dist/interfaces/api-key-repository.interface.d.ts.map +1 -0
- package/dist/interfaces/api-key-repository.interface.js +3 -0
- package/dist/interfaces/api-key-repository.interface.js.map +1 -0
- package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts +2 -0
- package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts.map +1 -1
- package/dist/interfaces/auth-user.interface.d.ts +28 -10
- package/dist/interfaces/auth-user.interface.d.ts.map +1 -1
- package/dist/interfaces/auth-user.interface.js.map +1 -1
- package/dist/interfaces/email-branding-config.interface.d.ts +10 -0
- package/dist/interfaces/email-branding-config.interface.d.ts.map +1 -0
- package/dist/interfaces/email-branding-config.interface.js +3 -0
- package/dist/interfaces/email-branding-config.interface.js.map +1 -0
- package/dist/interfaces/email-sender.interface.d.ts +13 -0
- package/dist/interfaces/email-sender.interface.d.ts.map +1 -0
- package/dist/interfaces/email-sender.interface.js +3 -0
- package/dist/interfaces/email-sender.interface.js.map +1 -0
- package/dist/interfaces/email-template-renderer.interface.d.ts +39 -0
- package/dist/interfaces/email-template-renderer.interface.d.ts.map +1 -0
- package/dist/interfaces/email-template-renderer.interface.js +3 -0
- package/dist/interfaces/email-template-renderer.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +3 -0
- package/dist/interfaces/index.d.ts.map +1 -1
- package/dist/interfaces/index.js +3 -0
- package/dist/interfaces/index.js.map +1 -1
- package/dist/interfaces/jwt-payload-factory.interface.d.ts +8 -0
- package/dist/interfaces/jwt-payload-factory.interface.d.ts.map +1 -0
- package/dist/interfaces/jwt-payload-factory.interface.js +3 -0
- package/dist/interfaces/jwt-payload-factory.interface.js.map +1 -0
- package/dist/interfaces/resource-permission-repository.interface.d.ts +4 -0
- package/dist/interfaces/resource-permission-repository.interface.d.ts.map +1 -0
- package/dist/interfaces/resource-permission-repository.interface.js +3 -0
- package/dist/interfaces/resource-permission-repository.interface.js.map +1 -0
- package/dist/interfaces/tenant-extractor.interface.d.ts +4 -0
- package/dist/interfaces/tenant-extractor.interface.d.ts.map +1 -0
- package/dist/interfaces/tenant-extractor.interface.js +3 -0
- package/dist/interfaces/tenant-extractor.interface.js.map +1 -0
- package/dist/interfaces/tenant-repository.interface.d.ts +9 -0
- package/dist/interfaces/tenant-repository.interface.d.ts.map +1 -0
- package/dist/interfaces/tenant-repository.interface.js +3 -0
- package/dist/interfaces/tenant-repository.interface.js.map +1 -0
- package/dist/interfaces/user-repository.interface.d.ts +26 -18
- package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
- package/dist/repositories/noop-brute-force.repository.d.ts +1 -1
- package/dist/repositories/noop-brute-force.repository.d.ts.map +1 -1
- package/dist/repositories/noop-brute-force.repository.js +6 -6
- package/dist/repositories/noop-brute-force.repository.js.map +1 -1
- package/dist/repositories/noop-tenant-extractor.d.ts +8 -0
- package/dist/repositories/noop-tenant-extractor.d.ts.map +1 -0
- package/dist/repositories/noop-tenant-extractor.js +35 -0
- package/dist/repositories/noop-tenant-extractor.js.map +1 -0
- package/dist/repositories/noop-tenant.repository.d.ts +8 -0
- package/dist/repositories/noop-tenant.repository.d.ts.map +1 -0
- package/dist/repositories/noop-tenant.repository.js +39 -0
- package/dist/repositories/noop-tenant.repository.js.map +1 -0
- package/dist/resolvers/base-auth.resolver.d.ts +18 -5
- package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
- package/dist/resolvers/base-auth.resolver.js +59 -25
- package/dist/resolvers/base-auth.resolver.js.map +1 -1
- package/dist/resolvers/oauth.controller.d.ts +1 -1
- package/dist/resolvers/oauth.controller.d.ts.map +1 -1
- package/dist/resolvers/oauth.controller.js +3 -2
- package/dist/resolvers/oauth.controller.js.map +1 -1
- package/dist/services/auth.service.d.ts +23 -3
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +173 -91
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/biometric-auth.service.d.ts +0 -1
- package/dist/services/biometric-auth.service.d.ts.map +1 -1
- package/dist/services/biometric-auth.service.js +0 -6
- package/dist/services/biometric-auth.service.js.map +1 -1
- package/dist/services/brute-force-protection.service.d.ts +2 -0
- package/dist/services/brute-force-protection.service.d.ts.map +1 -1
- package/dist/services/brute-force-protection.service.js +8 -0
- package/dist/services/brute-force-protection.service.js.map +1 -1
- package/dist/services/configurable-email.service.d.ts +23 -0
- package/dist/services/configurable-email.service.d.ts.map +1 -0
- package/dist/services/configurable-email.service.js +114 -0
- package/dist/services/configurable-email.service.js.map +1 -0
- package/dist/services/default-email-template-renderer.d.ts +57 -0
- package/dist/services/default-email-template-renderer.d.ts.map +1 -0
- package/dist/services/default-email-template-renderer.js +422 -0
- package/dist/services/default-email-template-renderer.js.map +1 -0
- package/dist/services/default-jwt-payload-factory.d.ts +9 -0
- package/dist/services/default-jwt-payload-factory.d.ts.map +1 -0
- package/dist/services/default-jwt-payload-factory.js +26 -0
- package/dist/services/default-jwt-payload-factory.js.map +1 -0
- package/dist/services/header-tenant-extractor.d.ts +7 -0
- package/dist/services/header-tenant-extractor.d.ts.map +1 -0
- package/dist/services/header-tenant-extractor.js +38 -0
- package/dist/services/header-tenant-extractor.js.map +1 -0
- package/dist/services/noop-email-sender.d.ts +15 -0
- package/dist/services/noop-email-sender.d.ts.map +1 -0
- package/dist/services/noop-email-sender.js +24 -0
- package/dist/services/noop-email-sender.js.map +1 -0
- package/dist/services/noop-email.service.d.ts +1 -0
- package/dist/services/noop-email.service.d.ts.map +1 -1
- package/dist/services/noop-email.service.js +7 -2
- package/dist/services/noop-email.service.js.map +1 -1
- package/dist/services/noop-sms.service.d.ts +1 -0
- package/dist/services/noop-sms.service.d.ts.map +1 -1
- package/dist/services/noop-sms.service.js +6 -1
- package/dist/services/noop-sms.service.js.map +1 -1
- package/dist/services/oauth-linking-token.service.d.ts.map +1 -1
- package/dist/services/oauth-linking-token.service.js +3 -8
- package/dist/services/oauth-linking-token.service.js.map +1 -1
- package/dist/services/refresh-token.service.d.ts +1 -0
- package/dist/services/refresh-token.service.d.ts.map +1 -1
- package/dist/services/refresh-token.service.js +15 -3
- package/dist/services/refresh-token.service.js.map +1 -1
- package/dist/services/resend-email-sender.d.ts +17 -0
- package/dist/services/resend-email-sender.d.ts.map +1 -0
- package/dist/services/resend-email-sender.js +45 -0
- package/dist/services/resend-email-sender.js.map +1 -0
- package/dist/services/sendgrid-email-sender.d.ts +16 -0
- package/dist/services/sendgrid-email-sender.d.ts.map +1 -0
- package/dist/services/sendgrid-email-sender.js +75 -0
- package/dist/services/sendgrid-email-sender.js.map +1 -0
- package/dist/services/sendgrid-email.service.d.ts.map +1 -1
- package/dist/services/sendgrid-email.service.js.map +1 -1
- package/dist/services/verification.service.d.ts +7 -0
- package/dist/services/verification.service.d.ts.map +1 -1
- package/dist/services/verification.service.js +104 -116
- package/dist/services/verification.service.js.map +1 -1
- package/dist/strategies/api-key.strategy.d.ts +11 -0
- package/dist/strategies/api-key.strategy.d.ts.map +1 -0
- package/dist/strategies/api-key.strategy.js +63 -0
- package/dist/strategies/api-key.strategy.js.map +1 -0
- package/dist/strategies/jwt.strategy.d.ts +6 -2
- package/dist/strategies/jwt.strategy.d.ts.map +1 -1
- package/dist/strategies/jwt.strategy.js +30 -4
- package/dist/strategies/jwt.strategy.js.map +1 -1
- package/dist/test-utils/mock-repositories.js +1 -1
- package/dist/test-utils/mock-repositories.js.map +1 -1
- package/dist/utils/cookie-options.d.ts +18 -0
- package/dist/utils/cookie-options.d.ts.map +1 -0
- package/dist/utils/cookie-options.js +65 -0
- package/dist/utils/cookie-options.js.map +1 -0
- package/dist/utils/escape-html.d.ts +2 -0
- package/dist/utils/escape-html.d.ts.map +1 -0
- package/dist/utils/escape-html.js +12 -0
- package/dist/utils/escape-html.js.map +1 -0
- package/dist/utils/execution-context.d.ts +3 -0
- package/dist/utils/execution-context.d.ts.map +1 -0
- package/dist/utils/execution-context.js +12 -0
- package/dist/utils/execution-context.js.map +1 -0
- package/dist/utils/request-helpers.d.ts +2 -0
- package/dist/utils/request-helpers.d.ts.map +1 -0
- package/dist/utils/request-helpers.js +9 -0
- package/dist/utils/request-helpers.js.map +1 -0
- package/package.json +13 -6
- package/dist/resolvers/auth.resolver.d.ts +0 -73
- package/dist/resolvers/auth.resolver.d.ts.map +0 -1
- package/dist/resolvers/auth.resolver.js +0 -472
- package/dist/resolvers/auth.resolver.js.map +0 -1
- package/dist/utils/passport-inspector.d.ts +0 -11
- package/dist/utils/passport-inspector.d.ts.map +0 -1
- package/dist/utils/passport-inspector.js +0 -48
- package/dist/utils/passport-inspector.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,1560 +1,775 @@
|
|
|
1
1
|
# @ambushsoftworks/nestjs-auth-graphql
|
|
2
2
|
|
|
3
|
-
Production-grade authentication
|
|
3
|
+
Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cookie auth, multi-tenancy, OAuth, email verification, and more -- with zero database coupling.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Design Principles](#design-principles)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Why BaseAuthResolver?](#why-baseauthresolver)
|
|
11
|
+
- [Features](#features)
|
|
12
|
+
- [Cookie Authentication](#cookie-authentication)
|
|
13
|
+
- [Multi-Tenancy](#multi-tenancy)
|
|
14
|
+
- [API Key Authentication](#api-key-authentication)
|
|
15
|
+
- [Email System](#email-system)
|
|
16
|
+
- [Verification Modes](#verification-modes)
|
|
17
|
+
- [OAuth](#oauth)
|
|
18
|
+
- [Brute Force Protection](#brute-force-protection)
|
|
19
|
+
- [Password Policy](#password-policy)
|
|
20
|
+
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
21
|
+
- [JWT Validation Modes](#jwt-validation-modes)
|
|
22
|
+
- [JWT Payload Factory](#jwt-payload-factory)
|
|
23
|
+
- [Configuration Reference](#configuration-reference)
|
|
24
|
+
- [Required Options](#required-options)
|
|
25
|
+
- [Common Options](#common-options)
|
|
26
|
+
- [Feature Flags](#feature-flags-features)
|
|
27
|
+
- [Security Secrets](#security-secrets)
|
|
28
|
+
- [Cookie Options](#cookie-options-cookie)
|
|
29
|
+
- [CSRF Options](#csrf-options-csrf)
|
|
30
|
+
- [Composable Email](#composable-email-email)
|
|
31
|
+
- [Verification Options](#verification-options-verification)
|
|
32
|
+
- [Optional Instance Options](#optional-instance-options)
|
|
33
|
+
- [Decorators](#decorators)
|
|
34
|
+
- [Guards](#guards)
|
|
35
|
+
- [Utilities](#utilities)
|
|
36
|
+
- [Security Features](#security-features)
|
|
37
|
+
- [License](#license)
|
|
4
38
|
|
|
5
39
|
---
|
|
6
40
|
|
|
7
|
-
##
|
|
41
|
+
## Design Principles
|
|
8
42
|
|
|
9
|
-
**
|
|
10
|
-
|
|
11
|
-
**
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- Package owns canonical enums (AuthProvider, UserStatus) - consumers use these in code
|
|
15
|
-
|
|
16
|
-
**Migration Time**: 2-4 hours | **Read**: [MIGRATION.md](./MIGRATION.md) | **Quick Start**: [QUICK-START.md](./QUICK-START.md)
|
|
17
|
-
|
|
18
|
-
**Why?** v0.2.0 eliminates GraphQL schema conflicts, provides full control over types, and works with any database/ORM across diverse projects.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Features
|
|
23
|
-
|
|
24
|
-
- **JWT Authentication**: Secure token-based authentication with automatic refresh
|
|
25
|
-
- **OAuth 2.0**: Google and Facebook social login
|
|
26
|
-
- **Email Verification**: 6-digit PIN codes via SendGrid with rate limiting
|
|
27
|
-
- **SMS Verification**: Phone number verification via Twilio
|
|
28
|
-
- **Password Reset**: 6-digit verification codes with email enumeration protection
|
|
29
|
-
- **Biometric Authentication**: Face ID, Touch ID, fingerprint support
|
|
30
|
-
- **Brute Force Protection**: Account lockout after failed login attempts
|
|
31
|
-
- **Account Linking**: Link/unlink social accounts to existing accounts
|
|
32
|
-
- **Security**: HMAC-SHA256 token hashing, AES-256-GCM encryption, constant-time comparison
|
|
33
|
-
- **Type Safety**: Full TypeScript support with strict mode
|
|
34
|
-
- **Testing**: 246+ tests from production codebase
|
|
43
|
+
- **Interface-driven persistence** -- All storage is behind interfaces (`IUserRepository`, `IRefreshTokenRepository`, `ITenantRepository`, etc.). Bring your own database.
|
|
44
|
+
- **Instance-based DI** -- Repositories and services are injected as instances via `useFactory`, not as classes.
|
|
45
|
+
- **Consumer owns GraphQL types** -- The package provides `BaseAuthResolver<T>` with `protected perform*()` methods. You create your own resolver with `@Mutation()` decorators (see [Why BaseAuthResolver?](#why-baseauthresolver)).
|
|
46
|
+
- **Guards are exported, not auto-registered** -- You register guards in your own `APP_GUARD` chain to control execution order.
|
|
47
|
+
- **NoOp fallbacks** -- Every optional dependency has a no-op implementation, so the module works with minimal config.
|
|
35
48
|
|
|
36
49
|
## Installation
|
|
37
50
|
|
|
38
51
|
```bash
|
|
39
|
-
npm install @
|
|
52
|
+
npm install @ambushsoftworks/nestjs-auth-graphql
|
|
40
53
|
```
|
|
41
54
|
|
|
42
55
|
### Peer Dependencies
|
|
43
56
|
|
|
44
57
|
```bash
|
|
45
|
-
npm install @nestjs/common @nestjs/core @nestjs/
|
|
58
|
+
npm install @nestjs/common @nestjs/core @nestjs/graphql @nestjs/jwt @nestjs/passport @nestjs/throttler graphql passport passport-jwt reflect-metadata rxjs
|
|
46
59
|
```
|
|
47
60
|
|
|
61
|
+
`resend` is an optional peer dependency -- install it only if using the Resend email sender.
|
|
62
|
+
|
|
48
63
|
## Quick Start
|
|
49
64
|
|
|
50
|
-
|
|
65
|
+
Minimal setup: JWT authentication with email/password login.
|
|
51
66
|
|
|
52
|
-
|
|
67
|
+
### 1. Implement the required repositories
|
|
53
68
|
|
|
54
69
|
```typescript
|
|
55
|
-
|
|
70
|
+
// users.repository.ts
|
|
56
71
|
import { Injectable } from '@nestjs/common';
|
|
57
|
-
import {
|
|
72
|
+
import { IUserRepositoryCore, CreateUserData } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
58
73
|
|
|
59
74
|
@Injectable()
|
|
60
|
-
export class
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
async
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async findByEmail(email: string): Promise<IAuthUser | null> {
|
|
68
|
-
return this.prisma.user.findUnique({ where: { email } });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async create(data: any): Promise<IAuthUser> {
|
|
72
|
-
return this.prisma.user.create({ data });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Implement other required methods...
|
|
75
|
+
export class UsersRepository implements IUserRepositoryCore<User> {
|
|
76
|
+
async findByEmail(email: string): Promise<User | null> { /* ... */ }
|
|
77
|
+
async findById(id: string): Promise<User | null> { /* ... */ }
|
|
78
|
+
async create(data: CreateUserData): Promise<User> { /* ... */ }
|
|
79
|
+
// ... implement remaining IUserRepositoryCore methods
|
|
76
80
|
}
|
|
81
|
+
```
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
async create(data: any): Promise<any> {
|
|
83
|
-
return this.prisma.refreshToken.create({ data });
|
|
84
|
-
}
|
|
83
|
+
```typescript
|
|
84
|
+
// refresh-token.repository.ts
|
|
85
|
+
import { Injectable } from '@nestjs/common';
|
|
86
|
+
import { IRefreshTokenRepository } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
@Injectable()
|
|
89
|
+
export class RefreshTokenRepository implements IRefreshTokenRepository {
|
|
90
|
+
async create(data: {
|
|
91
|
+
userId: string;
|
|
92
|
+
token: string;
|
|
93
|
+
hashedToken: string;
|
|
94
|
+
expiresAt: Date;
|
|
95
|
+
deviceInfo?: string | null;
|
|
96
|
+
}): Promise<any> { /* ... */ }
|
|
97
|
+
async findByHashedToken(hashedToken: string): Promise<any | null> { /* ... */ }
|
|
98
|
+
async deleteByUserId(userId: string): Promise<number> { /* ... */ }
|
|
99
|
+
// ... implement remaining methods
|
|
87
100
|
}
|
|
88
101
|
```
|
|
89
102
|
|
|
90
|
-
### 2.
|
|
91
|
-
|
|
92
|
-
**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.
|
|
103
|
+
### 2. Register the module
|
|
93
104
|
|
|
94
105
|
```typescript
|
|
95
106
|
import { Module } from '@nestjs/common';
|
|
96
|
-
import {
|
|
97
|
-
import {
|
|
98
|
-
import { PrismaModule } from './prisma/prisma.module';
|
|
99
|
-
import { PrismaUserRepository } from './repositories/prisma-user.repository';
|
|
100
|
-
import { PrismaRefreshTokenRepository } from './repositories/prisma-refresh-token.repository';
|
|
107
|
+
import { ConfigService } from '@nestjs/config';
|
|
108
|
+
import { AuthModule } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
101
109
|
|
|
102
110
|
@Module({
|
|
103
111
|
imports: [
|
|
104
|
-
ConfigModule.forRoot(),
|
|
105
|
-
PrismaModule, // Import module that provides repositories
|
|
106
112
|
AuthModule.forRootAsync({
|
|
107
|
-
imports: [
|
|
108
|
-
inject: [
|
|
109
|
-
|
|
110
|
-
PrismaRefreshTokenRepository,
|
|
111
|
-
ConfigService,
|
|
112
|
-
],
|
|
113
|
-
useFactory: (
|
|
114
|
-
usersRepo: PrismaUserRepository,
|
|
115
|
-
tokenRepo: PrismaRefreshTokenRepository,
|
|
116
|
-
config: ConfigService,
|
|
117
|
-
) => ({
|
|
118
|
-
// Pass instances directly (not classes)
|
|
113
|
+
imports: [ConfigModule, DatabaseModule],
|
|
114
|
+
inject: [UsersRepository, RefreshTokenRepository, ConfigService],
|
|
115
|
+
useFactory: (usersRepo, tokenRepo, config) => ({
|
|
119
116
|
userRepositoryInstance: usersRepo,
|
|
120
117
|
refreshTokenRepositoryInstance: tokenRepo,
|
|
121
|
-
|
|
122
|
-
// JWT configuration (required)
|
|
123
118
|
jwtSecret: config.get('JWT_SECRET'),
|
|
124
|
-
jwtExpiresIn: '15m', // Default: '15m'
|
|
125
|
-
refreshTokenExpiresIn: '30d', // Default: '30d'
|
|
126
|
-
|
|
127
|
-
// Feature flags
|
|
128
|
-
features: {
|
|
129
|
-
emailVerification: true,
|
|
130
|
-
smsVerification: true,
|
|
131
|
-
googleOAuth: true,
|
|
132
|
-
facebookOAuth: true,
|
|
133
|
-
biometricAuth: true,
|
|
134
|
-
bruteForceProtection: true,
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
// OAuth configuration
|
|
138
|
-
oauth: {
|
|
139
|
-
google: {
|
|
140
|
-
clientId: config.get('GOOGLE_CLIENT_ID'),
|
|
141
|
-
clientSecret: config.get('GOOGLE_CLIENT_SECRET'),
|
|
142
|
-
callbackUrl: config.get('GOOGLE_CALLBACK_URL'),
|
|
143
|
-
},
|
|
144
|
-
facebook: {
|
|
145
|
-
clientId: config.get('FACEBOOK_CLIENT_ID'),
|
|
146
|
-
clientSecret: config.get('FACEBOOK_CLIENT_SECRET'),
|
|
147
|
-
callbackUrl: config.get('FACEBOOK_CALLBACK_URL'),
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
119
|
}),
|
|
151
120
|
}),
|
|
152
121
|
],
|
|
153
|
-
providers: [
|
|
154
|
-
// Register repository classes so NestJS can inject them
|
|
155
|
-
PrismaUserRepository,
|
|
156
|
-
PrismaRefreshTokenRepository,
|
|
157
|
-
],
|
|
122
|
+
providers: [UsersRepository, RefreshTokenRepository],
|
|
158
123
|
})
|
|
159
124
|
export class AppModule {}
|
|
160
125
|
```
|
|
161
126
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
### 3. Create Your Auth Resolver
|
|
165
|
-
|
|
166
|
-
**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.
|
|
167
|
-
|
|
168
|
-
Create `src/auth/auth.resolver.ts`:
|
|
127
|
+
### 3. Create your auth resolver
|
|
169
128
|
|
|
170
129
|
```typescript
|
|
171
|
-
import { Resolver, Mutation, Args, Context
|
|
172
|
-
import {
|
|
173
|
-
import { Throttle } from '@nestjs/throttler';
|
|
130
|
+
import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
|
|
131
|
+
import { Inject } from '@nestjs/common';
|
|
174
132
|
import {
|
|
175
133
|
BaseAuthResolver,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
import { RefreshTokenInput } from './dto/refresh-token.input';
|
|
184
|
-
import { LogoutInput } from './dto/logout.input';
|
|
185
|
-
import { LogoutResponse, LogoutAllResponse } from './dto/logout-response.dto';
|
|
186
|
-
// ... import other DTOs for email/SMS verification, etc.
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Your app's auth resolver - extends BaseAuthResolver for business logic
|
|
190
|
-
*/
|
|
134
|
+
CurrentUser,
|
|
135
|
+
AuthService,
|
|
136
|
+
BruteForceProtectionService,
|
|
137
|
+
USER_REPOSITORY,
|
|
138
|
+
AUTH_LOGGER,
|
|
139
|
+
} from '@ambushsoftworks/nestjs-auth-graphql';
|
|
140
|
+
|
|
191
141
|
@Resolver()
|
|
192
142
|
export class AuthResolver extends BaseAuthResolver<User> {
|
|
193
|
-
|
|
143
|
+
constructor(
|
|
144
|
+
authService: AuthService,
|
|
145
|
+
bruteForceProtection: BruteForceProtectionService,
|
|
146
|
+
@Inject(USER_REPOSITORY) userRepository: any,
|
|
147
|
+
@Inject(AUTH_LOGGER) authLogger: any,
|
|
148
|
+
) {
|
|
149
|
+
super(authService, bruteForceProtection, userRepository, authLogger);
|
|
150
|
+
}
|
|
194
151
|
|
|
195
152
|
@Mutation(() => AuthResponse)
|
|
196
|
-
@
|
|
197
|
-
|
|
198
|
-
return this.performSignup(input) as Promise<AuthResponse>;
|
|
153
|
+
async signup(@Args('input') input: SignupInput, @Context() ctx: any) {
|
|
154
|
+
return this.performSignup(input, ctx);
|
|
199
155
|
}
|
|
200
156
|
|
|
201
157
|
@Mutation(() => AuthResponse)
|
|
202
|
-
@
|
|
203
|
-
|
|
204
|
-
@Args('input') input: LoginInput,
|
|
205
|
-
@Context() context: any,
|
|
206
|
-
): Promise<AuthResponse> {
|
|
207
|
-
return this.performLogin(input, context) as Promise<AuthResponse>;
|
|
158
|
+
async login(@Args('input') input: LoginInput, @Context() ctx: any) {
|
|
159
|
+
return this.performLogin(input, ctx);
|
|
208
160
|
}
|
|
209
161
|
|
|
210
162
|
@Mutation(() => AuthResponse)
|
|
211
|
-
@
|
|
212
|
-
|
|
213
|
-
return this.performRefreshToken(input) as Promise<AuthResponse>;
|
|
163
|
+
async refreshToken(@Args('input') input: RefreshTokenInput, @Context() ctx: any) {
|
|
164
|
+
return this.performRefreshToken(input, ctx);
|
|
214
165
|
}
|
|
215
166
|
|
|
216
167
|
@Mutation(() => LogoutResponse)
|
|
217
|
-
@
|
|
218
|
-
|
|
219
|
-
async logout(
|
|
220
|
-
@Args('input') input: LogoutInput,
|
|
221
|
-
@CurrentUser() user: User,
|
|
222
|
-
): Promise<LogoutResponse> {
|
|
223
|
-
return this.performLogout(input, user) as Promise<LogoutResponse>;
|
|
168
|
+
async logout(@Args('input') input: LogoutInput, @CurrentUser() user: User) {
|
|
169
|
+
return this.performLogout(input, user);
|
|
224
170
|
}
|
|
225
|
-
|
|
226
|
-
@Mutation(() => LogoutAllResponse)
|
|
227
|
-
@UseGuards(JwtAuthGuard)
|
|
228
|
-
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
|
229
|
-
async logoutAll(@CurrentUser() user: User): Promise<LogoutAllResponse> {
|
|
230
|
-
return this.performLogoutAll(user) as Promise<LogoutAllResponse>;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Add overrides for other mutations: verifyEmail, sendPhoneVerification, etc.
|
|
234
|
-
// See full example in Lift backend: src/auth/lift-auth.resolver.ts
|
|
235
171
|
}
|
|
236
172
|
```
|
|
237
173
|
|
|
238
|
-
|
|
239
|
-
- TypeScript decorator metadata is NOT preserved when importing classes from compiled npm packages
|
|
240
|
-
- This causes "Cannot determine GraphQL output type" errors at runtime
|
|
241
|
-
- Solution: Package provides `protected perform*()` methods, you add GraphQL decorators
|
|
242
|
-
|
|
243
|
-
**Register your resolver:**
|
|
174
|
+
### 4. Set up the guard chain
|
|
244
175
|
|
|
245
176
|
```typescript
|
|
177
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
178
|
+
import { createAuthGuard } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
179
|
+
|
|
246
180
|
@Module({
|
|
247
|
-
imports: [AuthModule.forRootAsync(...)],
|
|
248
181
|
providers: [
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
182
|
+
{
|
|
183
|
+
provide: APP_GUARD,
|
|
184
|
+
useClass: createAuthGuard(['jwt'], { allowPublic: true }),
|
|
185
|
+
},
|
|
252
186
|
],
|
|
253
187
|
})
|
|
254
188
|
export class AppModule {}
|
|
255
189
|
```
|
|
256
190
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
**IMPORTANT**: This package includes input validation using `class-validator` decorators on all input DTOs. You **must** enable NestJS's `ValidationPipe` globally for automatic validation.
|
|
260
|
-
|
|
261
|
-
Add this to your `main.ts`:
|
|
191
|
+
Mark public routes with `@Public()`:
|
|
262
192
|
|
|
263
193
|
```typescript
|
|
264
|
-
import {
|
|
265
|
-
import { ValidationPipe } from '@nestjs/common';
|
|
266
|
-
import { AppModule } from './app.module';
|
|
267
|
-
|
|
268
|
-
async function bootstrap() {
|
|
269
|
-
const app = await NestFactory.create(AppModule);
|
|
270
|
-
|
|
271
|
-
// Enable validation globally
|
|
272
|
-
app.useGlobalPipes(
|
|
273
|
-
new ValidationPipe({
|
|
274
|
-
whitelist: true, // Strip properties not in DTO
|
|
275
|
-
forbidNonWhitelisted: true, // Throw error if extra properties
|
|
276
|
-
transform: true, // Auto-transform payloads to DTO instances
|
|
277
|
-
transformOptions: {
|
|
278
|
-
enableImplicitConversion: true, // Auto-convert primitive types
|
|
279
|
-
},
|
|
280
|
-
}),
|
|
281
|
-
);
|
|
194
|
+
import { Public } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
282
195
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
196
|
+
@Public()
|
|
197
|
+
@Mutation(() => AuthResponse)
|
|
198
|
+
async login() { /* ... */ }
|
|
286
199
|
```
|
|
287
200
|
|
|
288
|
-
|
|
201
|
+
## Why BaseAuthResolver?
|
|
289
202
|
|
|
290
|
-
|
|
203
|
+
TypeScript decorator metadata is not preserved when importing from compiled npm packages. If the package exported a resolver with `@Mutation(() => AuthResponse)`, NestJS would throw "Cannot determine GraphQL output type" at runtime.
|
|
291
204
|
|
|
292
|
-
|
|
293
|
-
- **Password fields** (signup): `@MinLength(8)`, `@MaxLength(100)`, `@Matches()` - Enforces strong passwords with uppercase, lowercase, number, and special character
|
|
294
|
-
- **Password fields** (login): `@IsNotEmpty()` - Validates presence only (no strength check for existing passwords)
|
|
295
|
-
- **Verification codes**: `@Matches(/^\d{6}$/)` - Validates 6-digit PIN codes
|
|
296
|
-
- **Phone numbers**: `@Matches(/^\+[1-9]\d{1,14}$/)` - Validates E.164 international format (e.g., +14155552671)
|
|
297
|
-
- **Tokens**: `@IsString()`, `@IsNotEmpty()` - Validates refresh tokens and OAuth tokens
|
|
298
|
-
- **Device IDs**: `@IsUUID()` or `@IsString()` - Validates biometric device identifiers
|
|
299
|
-
- **Provider names**: `@IsIn(['google', 'facebook', 'apple'])` - Validates OAuth provider names
|
|
205
|
+
`BaseAuthResolver<T>` solves this by providing only business logic via `protected perform*()` methods. You add the GraphQL decorators in your own code, where metadata resolution works correctly.
|
|
300
206
|
|
|
301
|
-
|
|
207
|
+
When cookie auth is enabled, pass the GraphQL `context` so tokens are set as HttpOnly cookies. The response body will contain empty strings for `accessToken`/`refreshToken`.
|
|
302
208
|
|
|
303
|
-
|
|
209
|
+
### Available `perform*()` methods
|
|
304
210
|
|
|
305
|
-
|
|
306
|
-
{
|
|
307
|
-
"statusCode": 400,
|
|
308
|
-
"message": [
|
|
309
|
-
"Please provide a valid email address",
|
|
310
|
-
"Password must be at least 8 characters",
|
|
311
|
-
"Password must contain uppercase, lowercase, number, and special character"
|
|
312
|
-
],
|
|
313
|
-
"error": "Bad Request"
|
|
314
|
-
}
|
|
315
|
-
```
|
|
211
|
+
**Authentication:**
|
|
316
212
|
|
|
317
|
-
|
|
213
|
+
| Method | Purpose |
|
|
214
|
+
|--------|---------|
|
|
215
|
+
| `performSignup(input, context?)` | Create account |
|
|
216
|
+
| `performLogin(input, context)` | Authenticate |
|
|
217
|
+
| `performRefreshToken(input, context?)` | Rotate tokens |
|
|
218
|
+
| `performLogout(input, user, context?)` | Invalidate token |
|
|
219
|
+
| `performLogoutAll(user, context?)` | Invalidate all tokens |
|
|
220
|
+
| `performGetCurrentUser(userId)` | Get current user |
|
|
318
221
|
|
|
319
|
-
|
|
222
|
+
**Verification:**
|
|
320
223
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
email: string;
|
|
331
|
-
|
|
332
|
-
@Field()
|
|
333
|
-
@IsString()
|
|
334
|
-
@MinLength(12, { message: 'Password must be at least 12 characters' })
|
|
335
|
-
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/, {
|
|
336
|
-
message: 'Password must contain uppercase, lowercase, number, and special character',
|
|
337
|
-
})
|
|
338
|
-
password: string;
|
|
339
|
-
}
|
|
340
|
-
```
|
|
224
|
+
| Method | Purpose |
|
|
225
|
+
|--------|---------|
|
|
226
|
+
| `performVerifyEmail(input, context?)` | Email verification |
|
|
227
|
+
| `performResendVerificationEmail(email)` | Resend verification email |
|
|
228
|
+
| `performSendPhoneVerification(input, user)` | Send SMS verification code |
|
|
229
|
+
| `performVerifyPhone(input, user)` | Verify phone with SMS code |
|
|
230
|
+
| `performResendPhoneVerification(phoneNumber, user)` | Resend phone verification |
|
|
231
|
+
| `performRemovePhoneNumber(user)` | Remove phone number from account |
|
|
232
|
+
| `performPhoneVerificationStatus(user)` | Get phone verification status |
|
|
341
233
|
|
|
342
|
-
**
|
|
234
|
+
**Password:**
|
|
343
235
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
236
|
+
| Method | Purpose |
|
|
237
|
+
|--------|---------|
|
|
238
|
+
| `performRequestPasswordReset(input, context?)` | Send reset code/token |
|
|
239
|
+
| `performResetPassword(input, context?)` | Reset password |
|
|
240
|
+
| `performChangePassword(userId, current, new)` | Change password |
|
|
347
241
|
|
|
348
|
-
|
|
349
|
-
- `refreshTokenRepositoryInstance`: Instance of `IRefreshTokenRepository` implementation
|
|
350
|
-
- `jwtSecret`: Secret key for signing JWT tokens
|
|
242
|
+
**Other:**
|
|
351
243
|
|
|
352
|
-
|
|
244
|
+
| Method | Purpose |
|
|
245
|
+
|--------|---------|
|
|
246
|
+
| `performCheckAccountLockStatus(email)` | Check brute force lock status |
|
|
247
|
+
| `performCompleteFacebookSignUp(input)` | Facebook email fallback |
|
|
353
248
|
|
|
354
|
-
|
|
355
|
-
- Use `SendGridEmailService` for production
|
|
356
|
-
- Use `NoOpEmailService` for development/testing
|
|
357
|
-
- `smsServiceInstance`: Instance of `ISmsService` implementation (default: null)
|
|
358
|
-
- Use `TwilioSmsService` for production
|
|
359
|
-
- Use `NoOpSmsService` for development/testing
|
|
360
|
-
- `lifecycleHooksInstance`: Instance of `IAuthLifecycleHooks` implementation (default: null)
|
|
361
|
-
- Add custom logic for signup, login, logout, etc.
|
|
362
|
-
- `verificationRepositoryInstance`: Instance of `IVerificationRepository` implementation (default: NoOpVerificationRepository)
|
|
363
|
-
- `bruteForceRepositoryInstance`: Instance of `IBruteForceRepository` implementation (default: NoOpBruteForceRepository)
|
|
364
|
-
- `biometricRepositoryInstance`: Instance of `IBiometricRepository` implementation (default: NoOpBiometricRepository)
|
|
365
|
-
- `jwtExpiresIn`: JWT access token expiration time (default: '15m')
|
|
366
|
-
- `refreshTokenExpiresIn`: Refresh token expiration time (default: '30d')
|
|
367
|
-
|
|
368
|
-
### Feature Flags
|
|
369
|
-
|
|
370
|
-
Control which authentication features are enabled:
|
|
249
|
+
---
|
|
371
250
|
|
|
372
|
-
|
|
373
|
-
features: {
|
|
374
|
-
emailVerification: true, // Email verification with PIN codes
|
|
375
|
-
smsVerification: true, // SMS verification with Twilio
|
|
376
|
-
googleOAuth: true, // Google Sign In
|
|
377
|
-
facebookOAuth: true, // Facebook Login
|
|
378
|
-
biometricAuth: true, // Face ID, Touch ID, fingerprint
|
|
379
|
-
bruteForceProtection: true, // Account lockout after failed attempts
|
|
380
|
-
}
|
|
381
|
-
```
|
|
251
|
+
## Features
|
|
382
252
|
|
|
383
|
-
|
|
253
|
+
### Cookie Authentication
|
|
384
254
|
|
|
385
|
-
|
|
255
|
+
Enable `features.cookieAuth` to deliver tokens via HttpOnly cookies instead of response bodies.
|
|
386
256
|
|
|
387
257
|
```typescript
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
],
|
|
401
|
-
providers: [
|
|
402
|
-
SendGridEmailService, // Register in providers
|
|
403
|
-
],
|
|
258
|
+
AuthModule.forRootAsync({
|
|
259
|
+
useFactory: () => ({
|
|
260
|
+
// ...required options
|
|
261
|
+
features: { cookieAuth: true },
|
|
262
|
+
cookie: {
|
|
263
|
+
httpOnly: true, // default
|
|
264
|
+
secure: true, // default
|
|
265
|
+
sameSite: 'lax', // default
|
|
266
|
+
domain: undefined, // browser uses request domain
|
|
267
|
+
useHostPrefix: true, // prefix names with __Host- for enhanced security
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
404
270
|
})
|
|
405
|
-
|
|
406
|
-
// Environment variables required:
|
|
407
|
-
// SENDGRID_API_KEY=your_api_key
|
|
408
|
-
// SENDGRID_FROM_EMAIL=noreply@yourapp.com
|
|
409
|
-
// SENDGRID_FROM_NAME=Your App Name
|
|
410
271
|
```
|
|
411
272
|
|
|
412
|
-
|
|
273
|
+
When `useHostPrefix: true`, cookie names become `__Host-access_token` and `__Host-refresh_token`. The module validates at startup that `secure` is true, `path` is `/`, and `domain` is unset (as required by the `__Host-` spec).
|
|
413
274
|
|
|
414
|
-
|
|
415
|
-
import { TwilioSmsService } from '@yourorg/nestjs-auth-graphql';
|
|
275
|
+
You can also customize cookie names and max ages -- see [Cookie Options](#cookie-options-cookie).
|
|
416
276
|
|
|
417
|
-
|
|
418
|
-
imports: [
|
|
419
|
-
AuthModule.forRootAsync({
|
|
420
|
-
inject: [TwilioSmsService, /* ... */],
|
|
421
|
-
useFactory: (smsSvc, /* ... */) => ({
|
|
422
|
-
smsServiceInstance: smsSvc,
|
|
423
|
-
// ...
|
|
424
|
-
}),
|
|
425
|
-
}),
|
|
426
|
-
],
|
|
427
|
-
providers: [
|
|
428
|
-
TwilioSmsService, // Register in providers
|
|
429
|
-
],
|
|
430
|
-
})
|
|
277
|
+
The JWT strategy automatically reads from cookies first, then falls back to `Authorization: Bearer` headers.
|
|
431
278
|
|
|
432
|
-
|
|
433
|
-
// TWILIO_ACCOUNT_SID=your_account_sid
|
|
434
|
-
// TWILIO_AUTH_TOKEN=your_auth_token
|
|
435
|
-
// TWILIO_PHONE_NUMBER=+1234567890
|
|
436
|
-
```
|
|
279
|
+
#### CSRF Protection
|
|
437
280
|
|
|
438
|
-
|
|
281
|
+
With cookie auth, register `CsrfGuard` to protect mutations. It validates that a configurable header (default: `X-Requested-With`) is present when the request is authenticated via cookies.
|
|
439
282
|
|
|
440
283
|
```typescript
|
|
441
|
-
import {
|
|
284
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
285
|
+
import { createAuthGuard, CsrfGuard } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
442
286
|
|
|
443
287
|
@Module({
|
|
444
|
-
imports: [
|
|
445
|
-
AuthModule.forRootAsync({
|
|
446
|
-
inject: [NoOpEmailService, NoOpSmsService, /* ... */],
|
|
447
|
-
useFactory: (emailSvc, smsSvc, /* ... */) => ({
|
|
448
|
-
emailServiceInstance: emailSvc,
|
|
449
|
-
smsServiceInstance: smsSvc,
|
|
450
|
-
// ...
|
|
451
|
-
}),
|
|
452
|
-
}),
|
|
453
|
-
],
|
|
454
288
|
providers: [
|
|
455
|
-
|
|
456
|
-
|
|
289
|
+
{ provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) },
|
|
290
|
+
{ provide: APP_GUARD, useClass: CsrfGuard },
|
|
457
291
|
],
|
|
458
292
|
})
|
|
459
293
|
```
|
|
460
294
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
Secure password reset flow with 6-digit verification codes, rate limiting, and email enumeration protection.
|
|
464
|
-
|
|
465
|
-
### Features
|
|
466
|
-
|
|
467
|
-
- **6-Digit Verification Codes** - SMS/email verification pattern (not magic links)
|
|
468
|
-
- **Email Enumeration Protection** - Generic success messages for all requests
|
|
469
|
-
- **Rate Limiting** - 60-second cooldown between requests per user
|
|
470
|
-
- **Password Strength Validation** - Configurable requirements (default: 8+ chars, uppercase, lowercase, number)
|
|
471
|
-
- **Token Revocation** - All refresh tokens invalidated on password change
|
|
472
|
-
- **Brute Force Protection** - Integration with account locking system
|
|
473
|
-
- **OAuth User Protection** - Users authenticated via social login cannot reset passwords
|
|
474
|
-
- **Security Logging** - Audit trail for all password reset activities
|
|
475
|
-
|
|
476
|
-
### Consumer Setup
|
|
295
|
+
Configure via `csrf` options:
|
|
477
296
|
|
|
478
|
-
#### Step 1: Database Migration
|
|
479
|
-
|
|
480
|
-
Add `passwordResetSentAt` field to your User model:
|
|
481
|
-
|
|
482
|
-
**Prisma Example:**
|
|
483
|
-
```prisma
|
|
484
|
-
model User {
|
|
485
|
-
id String @id @default(cuid())
|
|
486
|
-
email String @unique
|
|
487
|
-
passwordHash String?
|
|
488
|
-
|
|
489
|
-
// Password reset rate limiting
|
|
490
|
-
passwordResetSentAt DateTime? // 60-second cooldown
|
|
491
|
-
|
|
492
|
-
// ... other fields
|
|
493
|
-
}
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
**TypeORM Example:**
|
|
497
297
|
```typescript
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
@Column({ nullable: true })
|
|
504
|
-
passwordResetSentAt: Date;
|
|
505
|
-
|
|
506
|
-
// ... other fields
|
|
298
|
+
csrf: {
|
|
299
|
+
headerName: 'X-Requested-With', // default
|
|
300
|
+
requireInProduction: true, // default
|
|
301
|
+
exemptOperations: ['refreshToken'], // skip CSRF for specific operations
|
|
507
302
|
}
|
|
508
303
|
```
|
|
509
304
|
|
|
510
|
-
|
|
305
|
+
The guard automatically skips when:
|
|
306
|
+
- The request uses `Authorization` header (Bearer/API key)
|
|
307
|
+
- The route has `@Public()`
|
|
308
|
+
- Cookie auth is not enabled
|
|
309
|
+
- `requireInProduction` is false and not in production
|
|
310
|
+
- The GraphQL operation is in `exemptOperations`
|
|
511
311
|
|
|
512
|
-
|
|
312
|
+
### Multi-Tenancy
|
|
313
|
+
|
|
314
|
+
Enable tenant resolution and permission checking across requests.
|
|
513
315
|
|
|
514
316
|
```typescript
|
|
515
317
|
AuthModule.forRootAsync({
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
emailServiceInstance: new SendGridEmailService(
|
|
522
|
-
config.get('SENDGRID_API_KEY'),
|
|
523
|
-
),
|
|
524
|
-
|
|
525
|
-
// IMPORTANT: Set frontend URL for email templates
|
|
526
|
-
// (Not used in 6-digit code flow, but required by email service)
|
|
527
|
-
features: {
|
|
528
|
-
emailVerification: true,
|
|
529
|
-
// ... other features
|
|
530
|
-
},
|
|
318
|
+
useFactory: (tenantRepo, tenantExtractor) => ({
|
|
319
|
+
// ...required options
|
|
320
|
+
features: { multiTenancy: true },
|
|
321
|
+
tenantRepositoryInstance: tenantRepo,
|
|
322
|
+
tenantExtractorInstance: tenantExtractor,
|
|
531
323
|
}),
|
|
532
|
-
})
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
**Environment Variable:**
|
|
536
|
-
```bash
|
|
537
|
-
FRONTEND_URL=https://yourapp.com # Used in email branding
|
|
324
|
+
})
|
|
538
325
|
```
|
|
539
326
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
Create consumer-specific DTOs with GraphQL decorators:
|
|
327
|
+
**Guard chain** (order matters):
|
|
543
328
|
|
|
544
|
-
**`request-password-reset.input.ts`:**
|
|
545
329
|
```typescript
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
@Field(() => String, {
|
|
552
|
-
description: 'User email address. Code sent if account exists.',
|
|
553
|
-
})
|
|
554
|
-
email: string;
|
|
555
|
-
}
|
|
330
|
+
providers: [
|
|
331
|
+
{ provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) },
|
|
332
|
+
{ provide: APP_GUARD, useClass: TenantGuard },
|
|
333
|
+
{ provide: APP_GUARD, useClass: PermissionGuard },
|
|
334
|
+
]
|
|
556
335
|
```
|
|
557
336
|
|
|
558
|
-
**`
|
|
559
|
-
```typescript
|
|
560
|
-
import { InputType, Field } from '@nestjs/graphql';
|
|
561
|
-
import { IAuthResetPasswordInput } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
562
|
-
|
|
563
|
-
@InputType()
|
|
564
|
-
export class ResetPasswordInput implements IAuthResetPasswordInput {
|
|
565
|
-
@Field(() => String)
|
|
566
|
-
email: string;
|
|
567
|
-
|
|
568
|
-
@Field(() => String, { description: '6-digit verification code' })
|
|
569
|
-
code: string;
|
|
337
|
+
**`ITenantRepository`** resolves whether a user has access to a tenant:
|
|
570
338
|
|
|
571
|
-
|
|
572
|
-
|
|
339
|
+
```typescript
|
|
340
|
+
@Injectable()
|
|
341
|
+
class TenantRepository implements ITenantRepository {
|
|
342
|
+
async resolveTenant(userId: string, tenantId: string): Promise<ITenantContext | null> {
|
|
343
|
+
const membership = await this.db.membership.find({ userId, tenantId });
|
|
344
|
+
if (!membership) return null;
|
|
345
|
+
return {
|
|
346
|
+
tenantId,
|
|
347
|
+
permissions: membership.role.permissions,
|
|
348
|
+
metadata: { accountId: membership.accountId },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
573
351
|
}
|
|
574
352
|
```
|
|
575
353
|
|
|
576
|
-
**`
|
|
577
|
-
```typescript
|
|
578
|
-
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
|
579
|
-
import { IAuthPasswordResetResponse } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
354
|
+
**`ITenantExtractor`** pulls the tenant ID from the request. Use the built-in `HeaderTenantExtractor` (reads `x-tenant-id` header) or implement your own:
|
|
580
355
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
@Field(() => Boolean)
|
|
584
|
-
success: boolean;
|
|
356
|
+
```typescript
|
|
357
|
+
import { HeaderTenantExtractor } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
585
358
|
|
|
586
|
-
|
|
587
|
-
|
|
359
|
+
// Default: reads x-tenant-id header
|
|
360
|
+
const extractor = new HeaderTenantExtractor();
|
|
588
361
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
362
|
+
// Custom header name
|
|
363
|
+
const extractor = new HeaderTenantExtractor('x-org-id');
|
|
592
364
|
```
|
|
593
365
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
Extend your custom resolver with password reset mutations:
|
|
366
|
+
**Access tenant context** in resolvers:
|
|
597
367
|
|
|
598
368
|
```typescript
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
import { RequestPasswordResetInput } from './dto/request-password-reset.input';
|
|
603
|
-
import { ResetPasswordInput } from './dto/reset-password.input';
|
|
604
|
-
import { PasswordResetResponse } from './dto/password-reset-response.dto';
|
|
605
|
-
import { User } from './entities/user.entity';
|
|
606
|
-
|
|
607
|
-
@Resolver()
|
|
608
|
-
export class AppAuthResolver extends BaseAuthResolver<User> {
|
|
609
|
-
// ... other mutations (signup, login, etc.)
|
|
610
|
-
|
|
611
|
-
@Mutation(() => PasswordResetResponse, {
|
|
612
|
-
name: 'requestPasswordReset',
|
|
613
|
-
description: 'Request password reset code via email',
|
|
614
|
-
})
|
|
615
|
-
@Throttle({ default: { limit: 3, ttl: 60000 } }) // 3 requests per minute
|
|
616
|
-
async requestPasswordReset(
|
|
617
|
-
@Args('input') input: RequestPasswordResetInput,
|
|
618
|
-
@Context() context: any,
|
|
619
|
-
): Promise<PasswordResetResponse> {
|
|
620
|
-
return this.performRequestPasswordReset(input, context) as Promise<PasswordResetResponse>;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
@Mutation(() => PasswordResetResponse, {
|
|
624
|
-
name: 'resetPassword',
|
|
625
|
-
description: 'Reset password using verification code',
|
|
626
|
-
})
|
|
627
|
-
@Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 attempts per 15 minutes
|
|
628
|
-
async resetPassword(
|
|
629
|
-
@Args('input') input: ResetPasswordInput,
|
|
630
|
-
@Context() context: any,
|
|
631
|
-
): Promise<PasswordResetResponse> {
|
|
632
|
-
return this.performResetPassword(input, context) as Promise<PasswordResetResponse>;
|
|
633
|
-
}
|
|
369
|
+
@Query(() => [Item])
|
|
370
|
+
async items(@CurrentTenant() tenant: ITenantContext) {
|
|
371
|
+
return this.service.findByTenant(tenant.tenantId);
|
|
634
372
|
}
|
|
635
373
|
```
|
|
636
374
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
```bash
|
|
640
|
-
# Run migration
|
|
641
|
-
npx prisma migrate deploy
|
|
642
|
-
|
|
643
|
-
# Start dev server
|
|
644
|
-
npm run start:dev
|
|
375
|
+
**Skip tenant resolution** for routes that don't need it:
|
|
645
376
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
377
|
+
```typescript
|
|
378
|
+
@SkipTenant()
|
|
379
|
+
@Query(() => User)
|
|
380
|
+
async me(@CurrentUser() user: User) { /* ... */ }
|
|
650
381
|
```
|
|
651
382
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
After setup, your schema will include:
|
|
383
|
+
**Permission checking** with `@RequirePermissions()`:
|
|
655
384
|
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
input RequestPasswordResetInput {
|
|
663
|
-
email: String!
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
input ResetPasswordInput {
|
|
667
|
-
email: String!
|
|
668
|
-
code: String!
|
|
669
|
-
newPassword: String!
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
type PasswordResetResponse {
|
|
673
|
-
success: Boolean!
|
|
674
|
-
message: String!
|
|
675
|
-
retryAfterSeconds: Int
|
|
676
|
-
}
|
|
385
|
+
```typescript
|
|
386
|
+
@RequirePermissions('clients:read')
|
|
387
|
+
@Query(() => [Client])
|
|
388
|
+
async clients(@CurrentTenant() tenant: ITenantContext) { /* ... */ }
|
|
677
389
|
```
|
|
678
390
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
**Expected Exceptions:**
|
|
682
|
-
|
|
683
|
-
| Exception | HTTP Status | When Thrown | Client Action |
|
|
684
|
-
|-----------|-------------|-------------|---------------|
|
|
685
|
-
| `PasswordResetRateLimitException` | 429 | < 60 seconds since last request | Display countdown: "Try again in X seconds" |
|
|
686
|
-
| `WeakPasswordException` | 400 | Password doesn't meet requirements | Show validation errors to user |
|
|
687
|
-
| `AccountLockedException` | 403 | Account locked due to brute force | Show "Account locked" message |
|
|
688
|
-
| `UnauthorizedException` | 401 | Invalid/expired code | "Code is invalid or expired" |
|
|
689
|
-
|
|
690
|
-
**Example Client-Side Error Handling (GraphQL):**
|
|
391
|
+
For resource-scoped permissions, combine with `@ResourceScope()` and provide an `IResourcePermissionRepository`:
|
|
691
392
|
|
|
692
393
|
```typescript
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
if (result.data?.resetPassword?.success) {
|
|
700
|
-
// Redirect to login
|
|
701
|
-
router.push('/login');
|
|
702
|
-
}
|
|
703
|
-
} catch (error) {
|
|
704
|
-
if (error.extensions?.code === 'BAD_REQUEST') {
|
|
705
|
-
// WeakPasswordException
|
|
706
|
-
showErrors(error.extensions.errors); // ["Password must contain uppercase", ...]
|
|
707
|
-
} else if (error.extensions?.code === 'TOO_MANY_REQUESTS') {
|
|
708
|
-
// PasswordResetRateLimitException
|
|
709
|
-
const retryAfter = error.extensions.retryAfterSeconds;
|
|
710
|
-
showCountdown(retryAfter);
|
|
711
|
-
} else if (error.message.includes('invalid or expired')) {
|
|
712
|
-
// UnauthorizedException
|
|
713
|
-
showError('Code is invalid or expired');
|
|
714
|
-
}
|
|
715
|
-
}
|
|
394
|
+
@RequirePermissions('clients:write')
|
|
395
|
+
@ResourceScope('client', 'clientId')
|
|
396
|
+
@Mutation(() => Client)
|
|
397
|
+
async updateClient(@Args('clientId') clientId: string) { /* ... */ }
|
|
716
398
|
```
|
|
717
399
|
|
|
718
|
-
###
|
|
719
|
-
|
|
720
|
-
1. **Never reveal whether email exists**
|
|
721
|
-
- Always return success message, even for non-existent emails
|
|
722
|
-
- Client cannot enumerate valid email addresses
|
|
400
|
+
### API Key Authentication
|
|
723
401
|
|
|
724
|
-
|
|
725
|
-
- Implement both per-user (60s) AND per-IP (via @Throttle) limits
|
|
726
|
-
- Prevents abuse and spam
|
|
727
|
-
|
|
728
|
-
3. **Code security**
|
|
729
|
-
- Codes are HMAC-SHA256 hashed in database (never plain text)
|
|
730
|
-
- Constant-time comparison prevents timing attacks
|
|
731
|
-
- 15-minute expiry limits exposure window
|
|
732
|
-
- Single-use enforcement prevents replay attacks
|
|
733
|
-
|
|
734
|
-
4. **Token revocation**
|
|
735
|
-
- All refresh tokens are invalidated on password change
|
|
736
|
-
- Forces re-authentication on all devices
|
|
737
|
-
- Prevents attacker from maintaining access
|
|
738
|
-
|
|
739
|
-
5. **Email template security**
|
|
740
|
-
- Do NOT include personalized reset URLs with embedded tokens
|
|
741
|
-
- Use 6-digit codes displayed in email (user manually enters in app)
|
|
742
|
-
- Prevents phishing attacks via link manipulation
|
|
743
|
-
|
|
744
|
-
### Lifecycle Hooks
|
|
745
|
-
|
|
746
|
-
Optionally track password reset events:
|
|
402
|
+
For machine-to-machine auth, provide an `IApiKeyRepository`:
|
|
747
403
|
|
|
748
404
|
```typescript
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
await this.analytics.track(user.id, 'password_reset_completed');
|
|
756
|
-
|
|
757
|
-
// Revoke API keys (if your app has them)
|
|
758
|
-
await this.apiKeyService.revokeAllKeys(user.id);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
405
|
+
AuthModule.forRootAsync({
|
|
406
|
+
useFactory: (apiKeyRepo) => ({
|
|
407
|
+
// ...required options
|
|
408
|
+
apiKeyRepositoryInstance: apiKeyRepo,
|
|
409
|
+
}),
|
|
410
|
+
})
|
|
761
411
|
```
|
|
762
412
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
The package provides a complete GraphQL API:
|
|
766
|
-
|
|
767
|
-
### Mutations
|
|
768
|
-
|
|
769
|
-
```graphql
|
|
770
|
-
# Signup
|
|
771
|
-
mutation Signup($input: SignupInput!) {
|
|
772
|
-
signup(signupInput: $input) {
|
|
773
|
-
accessToken
|
|
774
|
-
refreshToken
|
|
775
|
-
user { id email }
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
# Login
|
|
780
|
-
mutation Login($input: LoginInput!) {
|
|
781
|
-
login(loginInput: $input) {
|
|
782
|
-
accessToken
|
|
783
|
-
refreshToken
|
|
784
|
-
user { id email }
|
|
785
|
-
}
|
|
786
|
-
}
|
|
413
|
+
Then use a multi-strategy guard:
|
|
787
414
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
accessToken
|
|
792
|
-
refreshToken
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
# Logout
|
|
797
|
-
mutation Logout($input: LogoutInput!) {
|
|
798
|
-
logout(logoutInput: $input) {
|
|
799
|
-
success
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
# Email Verification
|
|
804
|
-
mutation VerifyEmail($input: VerifyEmailInput!) {
|
|
805
|
-
verifyEmail(verifyEmailInput: $input) {
|
|
806
|
-
success
|
|
807
|
-
user { id email emailVerified }
|
|
808
|
-
}
|
|
809
|
-
}
|
|
415
|
+
```typescript
|
|
416
|
+
{ provide: APP_GUARD, useClass: createAuthGuard(['api-key', 'jwt'], { allowPublic: true }) }
|
|
417
|
+
```
|
|
810
418
|
|
|
811
|
-
|
|
812
|
-
mutation VerifyPhone($input: VerifyPhoneInput!) {
|
|
813
|
-
verifyPhone(verifyPhoneInput: $input) {
|
|
814
|
-
success
|
|
815
|
-
user { id phoneNumber phoneVerified }
|
|
816
|
-
}
|
|
817
|
-
}
|
|
419
|
+
The `ApiKeyStrategy` hashes the bearer token with SHA-256 and calls `findByKeyHash()` on your repository. API keys and JWT tokens use the same `Authorization: Bearer` header -- strategies are tried in the order specified. When no bearer token is present (e.g. cookie auth), the API key strategy gracefully falls through to the next strategy.
|
|
818
420
|
|
|
819
|
-
|
|
820
|
-
mutation RequestPasswordReset($input: RequestPasswordResetInput!) {
|
|
821
|
-
requestPasswordReset(input: $input) {
|
|
822
|
-
success
|
|
823
|
-
message
|
|
824
|
-
retryAfterSeconds
|
|
825
|
-
}
|
|
826
|
-
}
|
|
421
|
+
### Email System
|
|
827
422
|
|
|
828
|
-
|
|
829
|
-
mutation ResetPassword($input: ResetPasswordInput!) {
|
|
830
|
-
resetPassword(input: $input) {
|
|
831
|
-
success
|
|
832
|
-
message
|
|
833
|
-
}
|
|
834
|
-
}
|
|
423
|
+
The email system uses a composable architecture: a **sender** (transport) and a **template renderer** (HTML generation).
|
|
835
424
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
linkGoogleAccount(linkGoogleAccountInput: $input) {
|
|
839
|
-
user { id googleId }
|
|
840
|
-
}
|
|
841
|
-
}
|
|
425
|
+
```typescript
|
|
426
|
+
import { SendGridEmailSender } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
842
427
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
428
|
+
AuthModule.forRootAsync({
|
|
429
|
+
useFactory: (config) => ({
|
|
430
|
+
// ...required options
|
|
431
|
+
email: {
|
|
432
|
+
sender: new SendGridEmailSender(config.get('SENDGRID_API_KEY')),
|
|
433
|
+
from: { email: 'noreply@example.com', name: 'MyApp' },
|
|
434
|
+
branding: {
|
|
435
|
+
appName: 'MyApp',
|
|
436
|
+
primaryColor: '#1976D2',
|
|
437
|
+
logoUrl: 'https://example.com/logo.png',
|
|
438
|
+
companyName: 'My Company',
|
|
439
|
+
supportEmail: 'support@example.com',
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
}),
|
|
443
|
+
})
|
|
850
444
|
```
|
|
851
445
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
```graphql
|
|
855
|
-
# Get current user
|
|
856
|
-
query Me {
|
|
857
|
-
me {
|
|
858
|
-
id
|
|
859
|
-
email
|
|
860
|
-
emailVerified
|
|
861
|
-
phoneVerified
|
|
862
|
-
googleId
|
|
863
|
-
facebookId
|
|
864
|
-
}
|
|
865
|
-
}
|
|
446
|
+
`email.branding` is required when using the default template renderer. If you provide a custom `email.templateRenderer`, branding can be omitted.
|
|
866
447
|
|
|
867
|
-
|
|
868
|
-
query CheckAccountLockStatus($email: String!) {
|
|
869
|
-
checkAccountLockStatus(email: $email) {
|
|
870
|
-
isLocked
|
|
871
|
-
remainingLockoutTime
|
|
872
|
-
}
|
|
873
|
-
}
|
|
448
|
+
**Built-in senders:** `SendGridEmailSender`, `ResendEmailSender`, `NoOpEmailSender`
|
|
874
449
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
450
|
+
**Custom sender:** Implement `IEmailSender`:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
class SmtpEmailSender implements IEmailSender {
|
|
454
|
+
async send(params: {
|
|
455
|
+
to: string;
|
|
456
|
+
from: { email: string; name?: string };
|
|
457
|
+
subject: string;
|
|
458
|
+
html: string;
|
|
459
|
+
text?: string;
|
|
460
|
+
}) {
|
|
461
|
+
// your SMTP logic
|
|
880
462
|
}
|
|
881
463
|
}
|
|
882
464
|
```
|
|
883
465
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
Add custom business logic to authentication events:
|
|
466
|
+
**Custom template renderer:** Override `DefaultEmailTemplateRenderer` by providing `email.templateRenderer`:
|
|
887
467
|
|
|
888
468
|
```typescript
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
export class MyAuthHooks implements IAuthLifecycleHooks {
|
|
894
|
-
async onSignup(user: IAuthUser): Promise<void> {
|
|
895
|
-
// Send welcome email, create default settings, etc.
|
|
896
|
-
console.log(`New user signed up: ${user.email}`);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
async onLogin(user: IAuthUser): Promise<void> {
|
|
900
|
-
// Update last login timestamp, log analytics, etc.
|
|
901
|
-
console.log(`User logged in: ${user.email}`);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
async onLogout(user: IAuthUser): Promise<void> {
|
|
905
|
-
// Clean up sessions, log analytics, etc.
|
|
906
|
-
console.log(`User logged out: ${user.email}`);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
async onEmailVerified(user: IAuthUser): Promise<void> {
|
|
910
|
-
// Send confirmation email, unlock features, etc.
|
|
911
|
-
console.log(`Email verified: ${user.email}`);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
async onPasswordReset(user: IAuthUser): Promise<void> {
|
|
915
|
-
// Log security event, notify user, etc.
|
|
916
|
-
console.log(`Password reset: ${user.email}`);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
async onAccountLocked(user: IAuthUser, unlockTime: Date): Promise<void> {
|
|
920
|
-
// Send notification, log security event, etc.
|
|
921
|
-
console.log(`Account locked: ${user.email} until ${unlockTime}`);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
async onSocialAccountLinked(user: IAuthUser, provider: string): Promise<void> {
|
|
925
|
-
// Send confirmation email, log event, etc.
|
|
926
|
-
console.log(`${provider} account linked: ${user.email}`);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
async onSocialAccountUnlinked(user: IAuthUser, provider: string): Promise<void> {
|
|
930
|
-
// Send confirmation email, log event, etc.
|
|
931
|
-
console.log(`${provider} account unlinked: ${user.email}`);
|
|
932
|
-
}
|
|
469
|
+
email: {
|
|
470
|
+
sender: new SendGridEmailSender(apiKey),
|
|
471
|
+
from: { email: 'noreply@example.com' },
|
|
472
|
+
templateRenderer: new MyCustomRenderer(),
|
|
933
473
|
}
|
|
934
474
|
```
|
|
935
475
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
### Brute Force Protection
|
|
939
|
-
|
|
940
|
-
- **5 failed login attempts** → 15-minute account lockout
|
|
941
|
-
- IP-based rate limiting: 10 attempts per minute
|
|
942
|
-
- Custom exception with `remainingLockoutTime` field
|
|
943
|
-
|
|
944
|
-
### Token Security
|
|
945
|
-
|
|
946
|
-
- **Access tokens**: Short-lived (15 minutes default)
|
|
947
|
-
- **Refresh tokens**: Long-lived (30 days), HMAC-SHA256 hashed
|
|
948
|
-
- **Token rotation**: New refresh token issued on each refresh
|
|
949
|
-
- **Idempotent refresh**: 10-second grace period prevents race conditions
|
|
950
|
-
|
|
951
|
-
### Verification Codes
|
|
952
|
-
|
|
953
|
-
- **6-digit PIN codes**
|
|
954
|
-
- **HMAC-SHA256 hashing**
|
|
955
|
-
- **15-minute expiry**
|
|
956
|
-
- **3 max attempts** per code
|
|
957
|
-
- **60-second rate limit** on resend
|
|
958
|
-
|
|
959
|
-
### OAuth Security
|
|
960
|
-
|
|
961
|
-
- **Stateless CSRF protection**: JWT-based state tokens
|
|
962
|
-
- **AES-256-GCM encryption**: OAuth access tokens encrypted at rest
|
|
963
|
-
- **Account linking**: Secure flow with linking tokens
|
|
964
|
-
|
|
965
|
-
### Biometric Authentication
|
|
966
|
-
|
|
967
|
-
- **Public key cryptography**: WebAuthn-compatible
|
|
968
|
-
- **Device enrollment**: Multiple device support
|
|
969
|
-
- **Challenge-response**: Prevents replay attacks
|
|
476
|
+
### Verification Modes
|
|
970
477
|
|
|
971
|
-
|
|
478
|
+
Two modes for email verification and password reset:
|
|
972
479
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
**CRITICAL**: Never commit secrets to version control. Use environment variables and secret management systems.
|
|
976
|
-
|
|
977
|
-
**Required Secrets:**
|
|
978
|
-
|
|
979
|
-
1. **JWT_SECRET**:
|
|
980
|
-
- Generate: `openssl rand -base64 64`
|
|
981
|
-
- Minimum: 32 bytes (256 bits)
|
|
982
|
-
- Rotate: Every 90 days or on suspected compromise
|
|
983
|
-
- Store: Environment variables, AWS Secrets Manager, HashiCorp Vault, etc.
|
|
984
|
-
|
|
985
|
-
2. **ENCRYPTION_KEY** (for OAuth):
|
|
986
|
-
- Generate: `openssl rand -hex 32` (produces 64-character hex string)
|
|
987
|
-
- Required length: Exactly 32 bytes (64 hex characters)
|
|
988
|
-
- Purpose: AES-256-GCM encryption for OAuth access tokens at rest
|
|
989
|
-
- Auto-generated if not provided, but **provide explicitly in production** for consistency across instances
|
|
990
|
-
|
|
991
|
-
3. **OAuth Secrets**:
|
|
992
|
-
- Never expose in client-side code
|
|
993
|
-
- Use environment-specific secrets (dev/staging/prod)
|
|
994
|
-
- Rotate on suspected compromise
|
|
995
|
-
|
|
996
|
-
**Example `.env` file** (never commit this file):
|
|
997
|
-
```bash
|
|
998
|
-
# Generate with: openssl rand -base64 64
|
|
999
|
-
JWT_SECRET=your_random_64_byte_base64_secret
|
|
1000
|
-
|
|
1001
|
-
# Generate with: openssl rand -hex 32
|
|
1002
|
-
ENCRYPTION_KEY=your_64_character_hex_string
|
|
1003
|
-
|
|
1004
|
-
# OAuth secrets from provider dashboards
|
|
1005
|
-
GOOGLE_CLIENT_SECRET=...
|
|
1006
|
-
FACEBOOK_CLIENT_SECRET=...
|
|
1007
|
-
|
|
1008
|
-
# Third-party API keys
|
|
1009
|
-
SENDGRID_API_KEY=...
|
|
1010
|
-
TWILIO_AUTH_TOKEN=...
|
|
1011
|
-
```
|
|
1012
|
-
|
|
1013
|
-
**Production Secret Management:**
|
|
480
|
+
- **`'code'`** (default) -- 6-digit numeric codes
|
|
481
|
+
- **`'token'`** -- URL-based tokens with configurable base URL
|
|
1014
482
|
|
|
1015
483
|
```typescript
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// ✅ GOOD: Environment variables
|
|
1024
|
-
AuthModule.forRootAsync({
|
|
1025
|
-
inject: [ConfigService],
|
|
1026
|
-
useFactory: (config: ConfigService) => ({
|
|
1027
|
-
jwtSecret: config.get<string>('JWT_SECRET'), // Read from env
|
|
1028
|
-
encryptionKey: config.get<string>('ENCRYPTION_KEY'),
|
|
1029
|
-
}),
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
// ✅ BETTER: Validation with config module
|
|
1033
|
-
import { ConfigModule } from '@nestjs/config';
|
|
1034
|
-
import * as Joi from 'joi';
|
|
1035
|
-
|
|
1036
|
-
ConfigModule.forRoot({
|
|
1037
|
-
validationSchema: Joi.object({
|
|
1038
|
-
JWT_SECRET: Joi.string().min(32).required(),
|
|
1039
|
-
ENCRYPTION_KEY: Joi.string().length(64).pattern(/^[0-9a-f]{64}$/).required(),
|
|
1040
|
-
GOOGLE_CLIENT_SECRET: Joi.string().when('GOOGLE_CLIENT_ID', {
|
|
1041
|
-
is: Joi.exist(),
|
|
1042
|
-
then: Joi.required(),
|
|
1043
|
-
}),
|
|
1044
|
-
}),
|
|
1045
|
-
});
|
|
484
|
+
features: { verificationMode: 'token' },
|
|
485
|
+
verification: {
|
|
486
|
+
baseUrl: 'https://app.example.com',
|
|
487
|
+
tokenLength: 64, // bytes, default: 64
|
|
488
|
+
tokenExpiresInMinutes: 60, // default: 60
|
|
489
|
+
},
|
|
1046
490
|
```
|
|
1047
491
|
|
|
1048
|
-
###
|
|
492
|
+
### OAuth
|
|
1049
493
|
|
|
1050
|
-
|
|
494
|
+
Google and Facebook OAuth with encrypted token storage (AES-256-GCM).
|
|
1051
495
|
|
|
1052
496
|
```typescript
|
|
1053
497
|
AuthModule.forRootAsync({
|
|
1054
498
|
useFactory: (config) => ({
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
```typescript
|
|
1070
|
-
AuthModule.forRootAsync({
|
|
1071
|
-
useFactory: (config) => ({
|
|
1072
|
-
// Balance between security and user experience
|
|
1073
|
-
refreshTokenExpiresIn: '30d', // Default: 30 days
|
|
1074
|
-
|
|
1075
|
-
// For high-security applications, use shorter expiration
|
|
1076
|
-
// refreshTokenExpiresIn: '7d', // Re-authenticate weekly
|
|
1077
|
-
|
|
1078
|
-
// For consumer apps, longer is acceptable
|
|
1079
|
-
// refreshTokenExpiresIn: '90d', // Re-authenticate quarterly
|
|
499
|
+
// ...required options
|
|
500
|
+
encryptionKey: config.get('ENCRYPTION_KEY'), // 32-byte hex string
|
|
501
|
+
oauth: {
|
|
502
|
+
google: {
|
|
503
|
+
clientId: config.get('GOOGLE_CLIENT_ID'),
|
|
504
|
+
clientSecret: config.get('GOOGLE_CLIENT_SECRET'),
|
|
505
|
+
callbackUrl: config.get('GOOGLE_CALLBACK_URL'),
|
|
506
|
+
},
|
|
507
|
+
facebook: {
|
|
508
|
+
clientId: config.get('FACEBOOK_CLIENT_ID'),
|
|
509
|
+
clientSecret: config.get('FACEBOOK_CLIENT_SECRET'),
|
|
510
|
+
callbackUrl: config.get('FACEBOOK_CALLBACK_URL'),
|
|
511
|
+
},
|
|
512
|
+
},
|
|
1080
513
|
}),
|
|
1081
|
-
})
|
|
514
|
+
})
|
|
1082
515
|
```
|
|
1083
516
|
|
|
1084
|
-
|
|
517
|
+
The `OAuthController` is automatically registered and handles OAuth callback routes. OAuth strategies gracefully degrade to no-ops when credentials are not configured.
|
|
1085
518
|
|
|
1086
|
-
###
|
|
519
|
+
### Brute Force Protection
|
|
1087
520
|
|
|
1088
|
-
|
|
521
|
+
When `features.bruteForceProtection` is enabled and a `bruteForceRepositoryInstance` is provided, the module tracks failed login attempts and temporarily locks accounts. The lockout policy (attempt thresholds, lockout duration) is determined by your `IBruteForceRepository` implementation.
|
|
1089
522
|
|
|
1090
|
-
|
|
523
|
+
### Password Policy
|
|
1091
524
|
|
|
1092
525
|
```typescript
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
{ message: 'Password must contain uppercase, lowercase, number, and special character' }
|
|
1109
|
-
)
|
|
1110
|
-
password: string;
|
|
526
|
+
passwordPolicy: {
|
|
527
|
+
minLength: 12, // default: 8
|
|
528
|
+
maxLength: 72, // bcrypt limit
|
|
529
|
+
requireUppercase: true, // default: true
|
|
530
|
+
requireLowercase: true, // default: true
|
|
531
|
+
requireNumber: true, // default: true
|
|
532
|
+
requireSpecialChar: true, // default: false
|
|
533
|
+
customValidator: async (password) => {
|
|
534
|
+
// Optional: dictionary checks, leaked password detection, etc.
|
|
535
|
+
const isCommon = await checkLeakedPasswords(password);
|
|
536
|
+
return {
|
|
537
|
+
isValid: !isCommon,
|
|
538
|
+
errors: isCommon ? ['Password found in data breaches'] : [],
|
|
539
|
+
};
|
|
540
|
+
},
|
|
1111
541
|
}
|
|
1112
542
|
```
|
|
1113
543
|
|
|
1114
|
-
|
|
1115
|
-
- ✅ Check against common password lists (e.g., `have-i-been-pwned` API)
|
|
1116
|
-
- ✅ Implement password history (prevent reuse of last 5 passwords)
|
|
1117
|
-
- ✅ Require password change on first login (for admin-created accounts)
|
|
1118
|
-
- ✅ Implement password expiration (60-90 days for high-security apps)
|
|
1119
|
-
|
|
1120
|
-
### ⏱️ Rate Limiting
|
|
544
|
+
### Lifecycle Hooks
|
|
1121
545
|
|
|
1122
|
-
|
|
546
|
+
React to auth events without modifying core logic:
|
|
1123
547
|
|
|
1124
548
|
```typescript
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
// See AuthResolver for @Throttle() decorators
|
|
1138
|
-
}),
|
|
1139
|
-
],
|
|
1140
|
-
})
|
|
549
|
+
lifecycleHooksInstance: {
|
|
550
|
+
async onSignup(user) { /* create default settings, send analytics */ },
|
|
551
|
+
async onLogin(user) { /* update metrics */ },
|
|
552
|
+
async onLogout(user) { /* cleanup */ },
|
|
553
|
+
async onEmailVerified(user) { /* unlock features */ },
|
|
554
|
+
async onPhoneVerified(user) { /* enable 2FA */ },
|
|
555
|
+
async onOAuthAccountLinked(user, provider) { /* sync data */ },
|
|
556
|
+
async onPasswordReset(user) { /* send security alert */ },
|
|
557
|
+
async onPasswordChanged(user) { /* notify user */ },
|
|
558
|
+
async onAccountDelete(user) { /* cleanup user data */ },
|
|
559
|
+
onAuthFailure(email, ip, reason) { /* security monitoring */ },
|
|
560
|
+
}
|
|
1141
561
|
```
|
|
1142
562
|
|
|
1143
|
-
|
|
1144
|
-
- Login: 5 requests/minute (prevent brute force)
|
|
1145
|
-
- Signup: 5 requests/minute (prevent spam)
|
|
1146
|
-
- Refresh: 10 requests/minute (allow frequent refreshes)
|
|
1147
|
-
- Verification codes: 3 requests/minute (prevent SMS/email bombing)
|
|
1148
|
-
|
|
1149
|
-
**Brute Force Protection**: Enabled by default - 5 failed login attempts = 15-minute account lockout.
|
|
563
|
+
All hooks are optional. Async hooks are awaited but failures are logged and do not block the auth operation. `onAuthFailure` is synchronous (fire-and-forget).
|
|
1150
564
|
|
|
1151
|
-
###
|
|
565
|
+
### JWT Validation Modes
|
|
1152
566
|
|
|
1153
|
-
|
|
567
|
+
- **`'full'`** (default) -- Calls `userRepository.findById()` on every request to verify the user still exists.
|
|
568
|
+
- **`'payload-only'`** -- Returns `{ id, email }` from the JWT payload without a DB lookup. Faster, but does not detect deleted/disabled users until token expiry.
|
|
1154
569
|
|
|
1155
570
|
```typescript
|
|
1156
|
-
|
|
1157
|
-
import { NestFactory } from '@nestjs/core';
|
|
1158
|
-
import { AppModule } from './app.module';
|
|
1159
|
-
|
|
1160
|
-
async function bootstrap() {
|
|
1161
|
-
const app = await NestFactory.create(AppModule);
|
|
1162
|
-
|
|
1163
|
-
// Enforce HTTPS in production
|
|
1164
|
-
if (process.env.NODE_ENV === 'production') {
|
|
1165
|
-
app.use((req, res, next) => {
|
|
1166
|
-
if (!req.secure && req.headers['x-forwarded-proto'] !== 'https') {
|
|
1167
|
-
return res.redirect(301, `https://${req.headers.host}${req.url}`);
|
|
1168
|
-
}
|
|
1169
|
-
next();
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
await app.listen(3000);
|
|
1174
|
-
}
|
|
1175
|
-
bootstrap();
|
|
571
|
+
jwtValidation: 'payload-only',
|
|
1176
572
|
```
|
|
1177
573
|
|
|
1178
|
-
|
|
574
|
+
### JWT Payload Factory
|
|
1179
575
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
**Implement Custom Security Logger** for production monitoring:
|
|
576
|
+
Customize JWT claims by providing an `IJwtPayloadFactory`:
|
|
1183
577
|
|
|
1184
578
|
```typescript
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
private readonly slackNotifier: SlackNotifier,
|
|
1193
|
-
) {}
|
|
1194
|
-
|
|
1195
|
-
log(event: SecurityEvent, metadata: Record<string, any>) {
|
|
1196
|
-
// Log to centralized logging service
|
|
1197
|
-
this.datadogClient.log({
|
|
1198
|
-
level: 'info',
|
|
1199
|
-
message: event,
|
|
1200
|
-
tags: ['auth', 'security'],
|
|
1201
|
-
...metadata,
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
// Alert on suspicious events
|
|
1205
|
-
if (event === SecurityEvent.TOKEN_REUSE_DETECTED) {
|
|
1206
|
-
this.slackNotifier.send(`⚠️ Security Alert: Token reuse detected for user ${metadata.userId}`);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
error(message: string, trace?: string, context?: string) {
|
|
1211
|
-
this.datadogClient.error({ message, trace, context });
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
warn(message: string, context?: string) {
|
|
1215
|
-
this.datadogClient.warn({ message, context });
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
debug(message: string, context?: string) {
|
|
1219
|
-
// Only in development
|
|
1220
|
-
if (process.env.NODE_ENV === 'development') {
|
|
1221
|
-
this.datadogClient.debug({ message, context });
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
verbose(message: string, context?: string) {
|
|
1226
|
-
// Only in development
|
|
1227
|
-
if (process.env.NODE_ENV === 'development') {
|
|
1228
|
-
this.datadogClient.log({ level: 'verbose', message, context });
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
579
|
+
jwtPayloadFactoryInstance: {
|
|
580
|
+
createPayload(user) {
|
|
581
|
+
return { sub: user.id, email: user.email, role: user.role };
|
|
582
|
+
},
|
|
583
|
+
extractUserId(payload) {
|
|
584
|
+
return payload.sub;
|
|
585
|
+
},
|
|
1231
586
|
}
|
|
1232
587
|
```
|
|
1233
588
|
|
|
1234
|
-
|
|
1235
|
-
- `TOKEN_REUSE_DETECTED`: Possible security breach
|
|
1236
|
-
- `ACCOUNT_LOCKED`: High failed login attempts
|
|
1237
|
-
- `LOGIN_FAILURE`: Pattern analysis for attacks
|
|
1238
|
-
- `VERIFICATION_CODE_FAILED`: Potential brute force on codes
|
|
1239
|
-
|
|
1240
|
-
### 🚀 Production Deployment
|
|
589
|
+
---
|
|
1241
590
|
|
|
1242
|
-
|
|
591
|
+
## Configuration Reference
|
|
1243
592
|
|
|
1244
|
-
|
|
1245
|
-
# Development (.env.development)
|
|
1246
|
-
NODE_ENV=development
|
|
1247
|
-
JWT_SECRET=dev_secret_key
|
|
1248
|
-
ENCRYPTION_KEY=dev_encryption_key
|
|
1249
|
-
|
|
1250
|
-
# Staging (.env.staging)
|
|
1251
|
-
NODE_ENV=staging
|
|
1252
|
-
JWT_SECRET=staging_secret_key_different_from_dev
|
|
1253
|
-
ENCRYPTION_KEY=staging_encryption_key_different_from_dev
|
|
1254
|
-
|
|
1255
|
-
# Production (.env.production)
|
|
1256
|
-
NODE_ENV=production
|
|
1257
|
-
JWT_SECRET=production_secret_key_from_secrets_manager
|
|
1258
|
-
ENCRYPTION_KEY=production_encryption_key_from_secrets_manager
|
|
1259
|
-
```
|
|
593
|
+
### Required Options
|
|
1260
594
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
595
|
+
| Option | Type | Description |
|
|
596
|
+
|--------|------|-------------|
|
|
597
|
+
| `userRepositoryInstance` | `IUserRepository` | User persistence |
|
|
598
|
+
| `refreshTokenRepositoryInstance` | `IRefreshTokenRepository` | Token persistence |
|
|
599
|
+
| `jwtSecret` | `string` | JWT signing secret |
|
|
600
|
+
|
|
601
|
+
### Common Options
|
|
602
|
+
|
|
603
|
+
| Option | Type | Default | Description |
|
|
604
|
+
|--------|------|---------|-------------|
|
|
605
|
+
| `jwtExpiresIn` | `string` | `'15m'` | Access token TTL |
|
|
606
|
+
| `refreshTokenExpiresIn` | `string` | `'30d'` | Refresh token TTL |
|
|
607
|
+
| `jwtValidation` | `'full' \| 'payload-only'` | `'full'` | JWT validation strategy |
|
|
608
|
+
| `bcryptRounds` | `number` | `12` | Password hashing cost |
|
|
609
|
+
| `nodeEnv` | `string` | `'production'` | Environment identifier |
|
|
610
|
+
| `passwordPolicy` | `PasswordPolicyConfig` | See [Password Policy](#password-policy) | Password strength rules |
|
|
611
|
+
| `encryptionKey` | `string` | -- | 32-byte hex string for AES-256-GCM (OAuth token encryption) |
|
|
612
|
+
|
|
613
|
+
### Feature Flags (`features`)
|
|
614
|
+
|
|
615
|
+
| Flag | Default | Description |
|
|
616
|
+
|------|---------|-------------|
|
|
617
|
+
| `cookieAuth` | `false` | Token delivery via HttpOnly cookies |
|
|
618
|
+
| `multiTenancy` | `false` | Enable TenantGuard + PermissionGuard |
|
|
619
|
+
| `bruteForceProtection` | `false` | Account lockout on failed logins |
|
|
620
|
+
| `preventEnumerationOnSignup` | `false` | Return synthetic success for duplicate emails |
|
|
621
|
+
| `verificationMode` | `'code'` | `'code'` for 6-digit codes, `'token'` for URL tokens |
|
|
622
|
+
| `emailVerification` | `false` | Enable email verification flow |
|
|
623
|
+
| `smsVerification` | `false` | Enable SMS verification flow |
|
|
624
|
+
| `googleOAuth` | `false` | Enable Google OAuth |
|
|
625
|
+
| `facebookOAuth` | `false` | Enable Facebook OAuth |
|
|
626
|
+
| `biometricAuth` | `false` | Enable biometric authentication |
|
|
627
|
+
|
|
628
|
+
### Security Secrets
|
|
629
|
+
|
|
630
|
+
Separate HMAC secrets for security isolation. All fall back to `jwtSecret` if not provided.
|
|
631
|
+
|
|
632
|
+
| Option | Type | Description |
|
|
633
|
+
|--------|------|-------------|
|
|
634
|
+
| `refreshTokenSecret` | `string` | HMAC secret for refresh token hashing |
|
|
635
|
+
| `verificationCodeSecret` | `string` | HMAC secret for verification code hashing |
|
|
636
|
+
| `oauthStateSecret` | `string` | JWT secret for OAuth CSRF state tokens |
|
|
637
|
+
| `oauthRedirectWhitelist` | `string` | Comma-separated allowed OAuth redirect URLs |
|
|
638
|
+
|
|
639
|
+
### Cookie Options (`cookie`)
|
|
640
|
+
|
|
641
|
+
| Option | Type | Default | Description |
|
|
642
|
+
|--------|------|---------|-------------|
|
|
643
|
+
| `httpOnly` | `boolean` | `true` | HttpOnly flag |
|
|
644
|
+
| `secure` | `boolean` | `true` | Secure flag |
|
|
645
|
+
| `sameSite` | `'lax' \| 'strict' \| 'none'` | `'lax'` | SameSite attribute |
|
|
646
|
+
| `domain` | `string` | `undefined` | Cookie domain |
|
|
647
|
+
| `path` | `string` | `'/'` | Cookie path |
|
|
648
|
+
| `accessTokenName` | `string` | `'access_token'` | Access token cookie name |
|
|
649
|
+
| `refreshTokenName` | `string` | `'refresh_token'` | Refresh token cookie name |
|
|
650
|
+
| `accessTokenMaxAge` | `number` | derived from `jwtExpiresIn` | Access token cookie max age (ms) |
|
|
651
|
+
| `refreshTokenMaxAge` | `number` | derived from `refreshTokenExpiresIn` | Refresh token cookie max age (ms) |
|
|
652
|
+
| `useHostPrefix` | `boolean` | `false` | Prefix names with `__Host-` |
|
|
653
|
+
|
|
654
|
+
### CSRF Options (`csrf`)
|
|
655
|
+
|
|
656
|
+
| Option | Type | Default | Description |
|
|
657
|
+
|--------|------|---------|-------------|
|
|
658
|
+
| `headerName` | `string` | `'X-Requested-With'` | Header to check |
|
|
659
|
+
| `requireInProduction` | `boolean` | `true` | Require CSRF header in production |
|
|
660
|
+
| `exemptOperations` | `string[]` | `[]` | GraphQL operations to skip |
|
|
661
|
+
|
|
662
|
+
### Composable Email (`email`)
|
|
663
|
+
|
|
664
|
+
| Option | Type | Description |
|
|
665
|
+
|--------|------|-------------|
|
|
666
|
+
| `email.sender` | `IEmailSender` | Transport (SendGrid, Resend, etc.) -- required |
|
|
667
|
+
| `email.from` | `{ email, name? }` | Sender address -- required |
|
|
668
|
+
| `email.branding` | `EmailBrandingConfig` | App name, colors, logo, etc. -- required unless `templateRenderer` is provided |
|
|
669
|
+
| `email.templateRenderer` | `IEmailTemplateRenderer` | Override default HTML renderer |
|
|
670
|
+
|
|
671
|
+
When `email` is provided, it takes precedence over `emailServiceInstance`.
|
|
672
|
+
|
|
673
|
+
### Verification Options (`verification`)
|
|
674
|
+
|
|
675
|
+
| Option | Type | Default | Description |
|
|
676
|
+
|--------|------|---------|-------------|
|
|
677
|
+
| `tokenLength` | `number` | `64` | Token length in bytes (token mode) |
|
|
678
|
+
| `tokenExpiresInMinutes` | `number` | `60` | Token expiration (token mode) |
|
|
679
|
+
| `baseUrl` | `string` | -- | Base URL for verification/reset URLs |
|
|
680
|
+
|
|
681
|
+
### Optional Instance Options
|
|
682
|
+
|
|
683
|
+
| Option | Interface | Fallback |
|
|
684
|
+
|--------|-----------|----------|
|
|
685
|
+
| `emailServiceInstance` | `IEmailService` | `NoOpEmailService` |
|
|
686
|
+
| `smsServiceInstance` | `ISmsService` | `NoOpSmsService` |
|
|
687
|
+
| `lifecycleHooksInstance` | `IAuthLifecycleHooks` | `{}` (no-op) |
|
|
688
|
+
| `verificationRepositoryInstance` | `IVerificationRepository` | `NoOpVerificationRepository` |
|
|
689
|
+
| `bruteForceRepositoryInstance` | `IBruteForceRepository` | `NoOpBruteForceRepository` |
|
|
690
|
+
| `biometricRepositoryInstance` | `IBiometricRepository` | `NoOpBiometricRepository` |
|
|
691
|
+
| `authLoggerInstance` | `IAuthLogger` | `ConsoleAuthLogger` |
|
|
692
|
+
| `tenantRepositoryInstance` | `ITenantRepository` | `null` |
|
|
693
|
+
| `tenantExtractorInstance` | `ITenantExtractor` | `null` |
|
|
694
|
+
| `resourcePermissionRepositoryInstance` | `IResourcePermissionRepository` | `null` |
|
|
695
|
+
| `jwtPayloadFactoryInstance` | `IJwtPayloadFactory` | `DefaultJwtPayloadFactory` |
|
|
696
|
+
| `apiKeyRepositoryInstance` | `IApiKeyRepository` | `null` |
|
|
697
|
+
| `rateLimiterInstance` | `IRateLimiter` | `InMemoryRateLimiterService` |
|
|
1280
698
|
|
|
1281
|
-
|
|
1282
|
-
import helmet from 'helmet';
|
|
1283
|
-
|
|
1284
|
-
async function bootstrap() {
|
|
1285
|
-
const app = await NestFactory.create(AppModule);
|
|
1286
|
-
|
|
1287
|
-
// Security headers
|
|
1288
|
-
app.use(helmet({
|
|
1289
|
-
contentSecurityPolicy: {
|
|
1290
|
-
directives: {
|
|
1291
|
-
defaultSrc: ["'self'"],
|
|
1292
|
-
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
1293
|
-
scriptSrc: ["'self'"],
|
|
1294
|
-
},
|
|
1295
|
-
},
|
|
1296
|
-
hsts: {
|
|
1297
|
-
maxAge: 31536000,
|
|
1298
|
-
includeSubDomains: true,
|
|
1299
|
-
preload: true,
|
|
1300
|
-
},
|
|
1301
|
-
}));
|
|
699
|
+
---
|
|
1302
700
|
|
|
1303
|
-
|
|
1304
|
-
}
|
|
1305
|
-
```
|
|
701
|
+
## Decorators
|
|
1306
702
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
**Monitoring Alerts:**
|
|
1317
|
-
- Failed login spike (>100/hour)
|
|
1318
|
-
- Account lockout spike (>10/hour)
|
|
1319
|
-
- Token reuse detection (any occurrence)
|
|
1320
|
-
- Unusual geographic login patterns
|
|
1321
|
-
- Multiple verification code failures
|
|
1322
|
-
|
|
1323
|
-
## Database Schema Requirements
|
|
1324
|
-
|
|
1325
|
-
This package expects specific database tables to implement the repository interfaces. You have two options:
|
|
1326
|
-
|
|
1327
|
-
### Option 1: Match the Package Schema (Recommended)
|
|
1328
|
-
|
|
1329
|
-
Create tables that directly match the package interfaces for optimal performance:
|
|
1330
|
-
|
|
1331
|
-
#### User Table
|
|
1332
|
-
|
|
1333
|
-
Required fields:
|
|
1334
|
-
```sql
|
|
1335
|
-
CREATE TABLE users (
|
|
1336
|
-
id VARCHAR(36) PRIMARY KEY,
|
|
1337
|
-
email VARCHAR(255) UNIQUE NOT NULL,
|
|
1338
|
-
passwordHash VARCHAR(255), -- Nullable for OAuth-only accounts
|
|
1339
|
-
emailVerified BOOLEAN DEFAULT false,
|
|
1340
|
-
phoneNumber VARCHAR(20), -- Nullable
|
|
1341
|
-
phoneVerified BOOLEAN DEFAULT false,
|
|
1342
|
-
googleId VARCHAR(255), -- Nullable, unique
|
|
1343
|
-
facebookId VARCHAR(255), -- Nullable, unique
|
|
1344
|
-
appleId VARCHAR(255), -- Nullable, unique
|
|
1345
|
-
authProvider VARCHAR(20) DEFAULT 'local', -- 'local' | 'google' | 'facebook' | 'apple'
|
|
1346
|
-
accountLockedUntil TIMESTAMP, -- Nullable, for brute force protection
|
|
1347
|
-
lastLoginAt TIMESTAMP,
|
|
1348
|
-
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1349
|
-
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
1350
|
-
);
|
|
1351
|
-
```
|
|
703
|
+
| Decorator | Target | Description |
|
|
704
|
+
|-----------|--------|-------------|
|
|
705
|
+
| `@Public()` | Method/Class | Skip authentication |
|
|
706
|
+
| `@PublicEndpoint()` | Method/Class | Skip authentication + tenant resolution (combines `@Public()` + `@SkipTenant()`) |
|
|
707
|
+
| `@SkipTenant()` | Method/Class | Skip tenant resolution |
|
|
708
|
+
| `@RequirePermissions('p1', 'p2')` | Method/Class | Require specific permissions |
|
|
709
|
+
| `@CurrentUser()` | Parameter | Inject authenticated user |
|
|
710
|
+
| `@CurrentTenant()` | Parameter | Inject resolved tenant context |
|
|
711
|
+
| `@ResourceScope('resourceType', 'argName')` | Method | Resource-level permission scoping |
|
|
1352
712
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
```sql
|
|
1356
|
-
CREATE TABLE refresh_tokens (
|
|
1357
|
-
id VARCHAR(36) PRIMARY KEY,
|
|
1358
|
-
userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
1359
|
-
token VARCHAR(255) NOT NULL, -- Plain token (sent to client)
|
|
1360
|
-
hashedToken VARCHAR(255) NOT NULL UNIQUE, -- HMAC-SHA256 hash for lookup
|
|
1361
|
-
expiresAt TIMESTAMP NOT NULL,
|
|
1362
|
-
deviceInfo TEXT, -- Nullable, JSON with device/browser info
|
|
1363
|
-
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1364
|
-
INDEX idx_userId (userId),
|
|
1365
|
-
INDEX idx_hashedToken (hashedToken),
|
|
1366
|
-
INDEX idx_expiresAt (expiresAt)
|
|
1367
|
-
);
|
|
1368
|
-
```
|
|
713
|
+
## Guards
|
|
1369
714
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
expiresAt TIMESTAMP NOT NULL,
|
|
1378
|
-
attempts INT DEFAULT 0,
|
|
1379
|
-
used BOOLEAN DEFAULT false,
|
|
1380
|
-
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1381
|
-
INDEX idx_userId (userId),
|
|
1382
|
-
UNIQUE idx_userId_unused (userId, used) -- Ensure only one active code per user
|
|
1383
|
-
);
|
|
1384
|
-
```
|
|
715
|
+
| Guard | Description |
|
|
716
|
+
|-------|-------------|
|
|
717
|
+
| `createAuthGuard(strategies, options)` | Factory for multi-strategy auth guards |
|
|
718
|
+
| `CsrfGuard` | CSRF header validation for cookie auth |
|
|
719
|
+
| `TenantGuard` | Multi-tenant context resolution |
|
|
720
|
+
| `PermissionGuard` | Permission checking against tenant context |
|
|
721
|
+
| `JwtAuthGuard` | Basic JWT guard (prefer `createAuthGuard`) |
|
|
1385
722
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
```sql
|
|
1389
|
-
CREATE TABLE phone_verifications (
|
|
1390
|
-
id VARCHAR(36) PRIMARY KEY,
|
|
1391
|
-
userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
1392
|
-
codeHash VARCHAR(255) NOT NULL, -- HMAC-SHA256 hash of 6-digit code
|
|
1393
|
-
expiresAt TIMESTAMP NOT NULL,
|
|
1394
|
-
attempts INT DEFAULT 0,
|
|
1395
|
-
used BOOLEAN DEFAULT false,
|
|
1396
|
-
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1397
|
-
INDEX idx_userId (userId),
|
|
1398
|
-
UNIQUE idx_userId_unused (userId, used)
|
|
1399
|
-
);
|
|
1400
|
-
```
|
|
723
|
+
### Recommended Guard Chains
|
|
1401
724
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
CREATE TABLE failed_login_attempts (
|
|
1406
|
-
id VARCHAR(36) PRIMARY KEY,
|
|
1407
|
-
userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
1408
|
-
ipAddress VARCHAR(45), -- IPv4 or IPv6
|
|
1409
|
-
attemptedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1410
|
-
INDEX idx_userId (userId),
|
|
1411
|
-
INDEX idx_attemptedAt (attemptedAt)
|
|
1412
|
-
);
|
|
1413
|
-
```
|
|
725
|
+
```typescript
|
|
726
|
+
// JWT only
|
|
727
|
+
{ provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) }
|
|
1414
728
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
```sql
|
|
1418
|
-
CREATE TABLE biometric_credentials (
|
|
1419
|
-
id VARCHAR(36) PRIMARY KEY,
|
|
1420
|
-
userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
1421
|
-
credentialId VARCHAR(255) NOT NULL UNIQUE, -- WebAuthn credential ID
|
|
1422
|
-
publicKey TEXT NOT NULL, -- PEM-encoded ECDSA public key
|
|
1423
|
-
deviceName VARCHAR(255) NOT NULL,
|
|
1424
|
-
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1425
|
-
lastUsedAt TIMESTAMP,
|
|
1426
|
-
INDEX idx_userId (userId),
|
|
1427
|
-
INDEX idx_credentialId (credentialId)
|
|
1428
|
-
);
|
|
1429
|
-
```
|
|
729
|
+
// JWT + API keys
|
|
730
|
+
{ provide: APP_GUARD, useClass: createAuthGuard(['jwt', 'api-key'], { allowPublic: true }) }
|
|
1430
731
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
1437
|
-
challenge VARCHAR(255) NOT NULL, -- Base64-encoded random challenge
|
|
1438
|
-
expiresAt TIMESTAMP NOT NULL,
|
|
1439
|
-
used BOOLEAN DEFAULT false,
|
|
1440
|
-
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1441
|
-
INDEX idx_userId (userId),
|
|
1442
|
-
INDEX idx_expiresAt (expiresAt)
|
|
1443
|
-
);
|
|
732
|
+
// Full stack with cookie auth + multi-tenancy
|
|
733
|
+
{ provide: APP_GUARD, useClass: CsrfGuard },
|
|
734
|
+
{ provide: APP_GUARD, useClass: createAuthGuard(['jwt', 'api-key'], { allowPublic: true }) },
|
|
735
|
+
{ provide: APP_GUARD, useClass: TenantGuard },
|
|
736
|
+
{ provide: APP_GUARD, useClass: PermissionGuard },
|
|
1444
737
|
```
|
|
1445
738
|
|
|
1446
|
-
|
|
739
|
+
## Utilities
|
|
1447
740
|
|
|
1448
|
-
|
|
741
|
+
Helper functions exported for use in custom guards and middleware:
|
|
1449
742
|
|
|
1450
|
-
|
|
743
|
+
| Function | Description |
|
|
744
|
+
|----------|-------------|
|
|
745
|
+
| `getRequestFromContext(context)` | Extract request from `ExecutionContext` (handles GraphQL + HTTP) |
|
|
746
|
+
| `extractIpAddress(request)` | Extract client IP (checks `clientIp`, `req.ip`, falls back to `'unknown'`) |
|
|
1451
747
|
|
|
1452
748
|
```typescript
|
|
749
|
+
import { getRequestFromContext, extractIpAddress } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
750
|
+
|
|
1453
751
|
@Injectable()
|
|
1454
|
-
export class
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
expiresAt: Date,
|
|
1461
|
-
type: 'email' | 'phone',
|
|
1462
|
-
): Promise<void> {
|
|
1463
|
-
// 1. Look up user to get email or phone (extra query)
|
|
1464
|
-
const user = await this.prisma.user.findUnique({
|
|
1465
|
-
where: { id: userId },
|
|
1466
|
-
select: { email: true, phoneNumber: true },
|
|
1467
|
-
});
|
|
1468
|
-
|
|
1469
|
-
const identifier = type === 'email' ? user.email : user.phoneNumber;
|
|
1470
|
-
|
|
1471
|
-
// 2. Store in unified VerificationCode table
|
|
1472
|
-
await this.prisma.verificationCode.create({
|
|
1473
|
-
data: {
|
|
1474
|
-
type: type === 'email' ? 'email' : 'sms',
|
|
1475
|
-
identifier: identifier.toLowerCase(),
|
|
1476
|
-
codeHash: code,
|
|
1477
|
-
expiresAt,
|
|
1478
|
-
attempts: 0,
|
|
1479
|
-
used: false,
|
|
1480
|
-
},
|
|
1481
|
-
});
|
|
752
|
+
export class CustomGuard implements CanActivate {
|
|
753
|
+
canActivate(context: ExecutionContext): boolean {
|
|
754
|
+
const request = getRequestFromContext(context);
|
|
755
|
+
const ip = extractIpAddress(request);
|
|
756
|
+
// your logic
|
|
757
|
+
return true;
|
|
1482
758
|
}
|
|
1483
|
-
|
|
1484
|
-
// Implement other methods with similar adapter logic...
|
|
1485
759
|
}
|
|
1486
760
|
```
|
|
1487
761
|
|
|
1488
|
-
|
|
1489
|
-
- ✅ **Pro**: No schema migration required, preserves existing logic
|
|
1490
|
-
- ❌ **Con**: Extra database queries (performance overhead)
|
|
1491
|
-
- ❌ **Con**: Adapter complexity increases maintenance burden
|
|
1492
|
-
|
|
1493
|
-
**Recommendation**: Use Option 1 for new projects. Use Option 2 for migrating existing apps with established schemas.
|
|
1494
|
-
|
|
1495
|
-
## Environment Variables
|
|
1496
|
-
|
|
1497
|
-
```bash
|
|
1498
|
-
# JWT
|
|
1499
|
-
JWT_SECRET=your_secret_key_here
|
|
1500
|
-
|
|
1501
|
-
# SendGrid (if using SendGridEmailService)
|
|
1502
|
-
SENDGRID_API_KEY=your_sendgrid_api_key
|
|
1503
|
-
SENDGRID_FROM_EMAIL=noreply@yourapp.com
|
|
1504
|
-
SENDGRID_FROM_NAME=Your App Name
|
|
1505
|
-
|
|
1506
|
-
# Twilio (if using TwilioSmsService)
|
|
1507
|
-
TWILIO_ACCOUNT_SID=your_twilio_account_sid
|
|
1508
|
-
TWILIO_AUTH_TOKEN=your_twilio_auth_token
|
|
1509
|
-
TWILIO_PHONE_NUMBER=+1234567890
|
|
1510
|
-
|
|
1511
|
-
# Google OAuth (if using)
|
|
1512
|
-
GOOGLE_CLIENT_ID=your_google_client_id
|
|
1513
|
-
GOOGLE_CLIENT_SECRET=your_google_client_secret
|
|
1514
|
-
GOOGLE_CALLBACK_URL=https://yourapp.com/api/auth/google/callback
|
|
1515
|
-
|
|
1516
|
-
# Facebook OAuth (if using)
|
|
1517
|
-
FACEBOOK_CLIENT_ID=your_facebook_app_id
|
|
1518
|
-
FACEBOOK_CLIENT_SECRET=your_facebook_app_secret
|
|
1519
|
-
FACEBOOK_CALLBACK_URL=https://yourapp.com/api/auth/facebook/callback
|
|
1520
|
-
|
|
1521
|
-
# Encryption (auto-generated if not provided)
|
|
1522
|
-
ENCRYPTION_KEY=32_byte_hex_string
|
|
1523
|
-
```
|
|
1524
|
-
|
|
1525
|
-
## Testing
|
|
1526
|
-
|
|
1527
|
-
The package includes 246+ tests from the production Lift app:
|
|
1528
|
-
|
|
1529
|
-
```bash
|
|
1530
|
-
npm test # Run all tests
|
|
1531
|
-
npm run test:watch # Watch mode
|
|
1532
|
-
npm run test:cov # Coverage report
|
|
1533
|
-
```
|
|
1534
|
-
|
|
1535
|
-
## Architecture
|
|
1536
|
-
|
|
1537
|
-
This package follows **Layer 0 architecture** with complete interface abstraction:
|
|
1538
|
-
|
|
1539
|
-
- **No database coupling**: All services use interfaces (`IUserRepository`, etc.)
|
|
1540
|
-
- **No Prisma imports** in package code
|
|
1541
|
-
- **Dependency injection**: Proper NestJS DI patterns
|
|
1542
|
-
- **Type safety**: Strict TypeScript mode enabled
|
|
1543
|
-
- **Production tested**: Extracted from Lift app with 246+ passing tests
|
|
1544
|
-
|
|
1545
|
-
## Migration from Lift Codebase
|
|
1546
|
-
|
|
1547
|
-
If you're migrating from the Lift codebase:
|
|
762
|
+
## Security Features
|
|
1548
763
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
764
|
+
- **Refresh token rotation** with HMAC-SHA256 hashing and idempotent 10-second grace period
|
|
765
|
+
- **Bcrypt password hashing** with configurable rounds (default: 12)
|
|
766
|
+
- **AES-256-GCM encryption** for OAuth tokens at rest
|
|
767
|
+
- **Constant-time comparison** for verification codes
|
|
768
|
+
- **Account enumeration prevention** on signup (optional)
|
|
769
|
+
- **Stateless CSRF protection** for OAuth flows via `OAuthStateService`
|
|
770
|
+
- **`__Host-` cookie prefix** support for enhanced cookie security
|
|
771
|
+
- **Separate HMAC secrets** for refresh tokens, verification codes, and OAuth state
|
|
1553
772
|
|
|
1554
773
|
## License
|
|
1555
774
|
|
|
1556
|
-
MIT
|
|
1557
|
-
|
|
1558
|
-
## Support
|
|
1559
|
-
|
|
1560
|
-
For issues and questions, please open an issue on GitLab.
|
|
775
|
+
MIT -- see [LICENSE](./LICENSE).
|