@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.
Files changed (212) hide show
  1. package/README.md +528 -1313
  2. package/dist/auth.module.d.ts +53 -2
  3. package/dist/auth.module.d.ts.map +1 -1
  4. package/dist/auth.module.js +115 -16
  5. package/dist/auth.module.js.map +1 -1
  6. package/dist/constants.d.ts +13 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +14 -1
  9. package/dist/constants.js.map +1 -1
  10. package/dist/decorators/current-tenant.decorator.d.ts +2 -0
  11. package/dist/decorators/current-tenant.decorator.d.ts.map +1 -0
  12. package/dist/decorators/current-tenant.decorator.js +10 -0
  13. package/dist/decorators/current-tenant.decorator.js.map +1 -0
  14. package/dist/decorators/public-endpoint.decorator.d.ts +2 -0
  15. package/dist/decorators/public-endpoint.decorator.d.ts.map +1 -0
  16. package/dist/decorators/public-endpoint.decorator.js +9 -0
  17. package/dist/decorators/public-endpoint.decorator.js.map +1 -0
  18. package/dist/decorators/public.decorator.d.ts +3 -0
  19. package/dist/decorators/public.decorator.d.ts.map +1 -0
  20. package/dist/decorators/public.decorator.js +8 -0
  21. package/dist/decorators/public.decorator.js.map +1 -0
  22. package/dist/decorators/require-permissions.decorator.d.ts +2 -0
  23. package/dist/decorators/require-permissions.decorator.d.ts.map +1 -0
  24. package/dist/decorators/require-permissions.decorator.js +8 -0
  25. package/dist/decorators/require-permissions.decorator.js.map +1 -0
  26. package/dist/decorators/resource-scope.decorator.d.ts +2 -0
  27. package/dist/decorators/resource-scope.decorator.d.ts.map +1 -0
  28. package/dist/decorators/resource-scope.decorator.js +8 -0
  29. package/dist/decorators/resource-scope.decorator.js.map +1 -0
  30. package/dist/decorators/skip-tenant.decorator.d.ts +2 -0
  31. package/dist/decorators/skip-tenant.decorator.d.ts.map +1 -0
  32. package/dist/decorators/skip-tenant.decorator.js +8 -0
  33. package/dist/decorators/skip-tenant.decorator.js.map +1 -0
  34. package/dist/guards/create-auth-guard.d.ts +11 -0
  35. package/dist/guards/create-auth-guard.d.ts.map +1 -0
  36. package/dist/guards/create-auth-guard.js +49 -0
  37. package/dist/guards/create-auth-guard.js.map +1 -0
  38. package/dist/guards/csrf.guard.d.ts +16 -0
  39. package/dist/guards/csrf.guard.d.ts.map +1 -0
  40. package/dist/guards/csrf.guard.js +90 -0
  41. package/dist/guards/csrf.guard.js.map +1 -0
  42. package/dist/guards/permission.guard.d.ts +12 -0
  43. package/dist/guards/permission.guard.d.ts.map +1 -0
  44. package/dist/guards/permission.guard.js +90 -0
  45. package/dist/guards/permission.guard.js.map +1 -0
  46. package/dist/guards/tenant.guard.d.ts +13 -0
  47. package/dist/guards/tenant.guard.d.ts.map +1 -0
  48. package/dist/guards/tenant.guard.js +85 -0
  49. package/dist/guards/tenant.guard.js.map +1 -0
  50. package/dist/index.d.ts +32 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +32 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/interfaces/api-key-repository.interface.d.ts +11 -0
  55. package/dist/interfaces/api-key-repository.interface.d.ts.map +1 -0
  56. package/dist/interfaces/api-key-repository.interface.js +3 -0
  57. package/dist/interfaces/api-key-repository.interface.js.map +1 -0
  58. package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts +2 -0
  59. package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts.map +1 -1
  60. package/dist/interfaces/auth-user.interface.d.ts +28 -10
  61. package/dist/interfaces/auth-user.interface.d.ts.map +1 -1
  62. package/dist/interfaces/auth-user.interface.js.map +1 -1
  63. package/dist/interfaces/email-branding-config.interface.d.ts +10 -0
  64. package/dist/interfaces/email-branding-config.interface.d.ts.map +1 -0
  65. package/dist/interfaces/email-branding-config.interface.js +3 -0
  66. package/dist/interfaces/email-branding-config.interface.js.map +1 -0
  67. package/dist/interfaces/email-sender.interface.d.ts +13 -0
  68. package/dist/interfaces/email-sender.interface.d.ts.map +1 -0
  69. package/dist/interfaces/email-sender.interface.js +3 -0
  70. package/dist/interfaces/email-sender.interface.js.map +1 -0
  71. package/dist/interfaces/email-template-renderer.interface.d.ts +39 -0
  72. package/dist/interfaces/email-template-renderer.interface.d.ts.map +1 -0
  73. package/dist/interfaces/email-template-renderer.interface.js +3 -0
  74. package/dist/interfaces/email-template-renderer.interface.js.map +1 -0
  75. package/dist/interfaces/index.d.ts +3 -0
  76. package/dist/interfaces/index.d.ts.map +1 -1
  77. package/dist/interfaces/index.js +3 -0
  78. package/dist/interfaces/index.js.map +1 -1
  79. package/dist/interfaces/jwt-payload-factory.interface.d.ts +8 -0
  80. package/dist/interfaces/jwt-payload-factory.interface.d.ts.map +1 -0
  81. package/dist/interfaces/jwt-payload-factory.interface.js +3 -0
  82. package/dist/interfaces/jwt-payload-factory.interface.js.map +1 -0
  83. package/dist/interfaces/resource-permission-repository.interface.d.ts +4 -0
  84. package/dist/interfaces/resource-permission-repository.interface.d.ts.map +1 -0
  85. package/dist/interfaces/resource-permission-repository.interface.js +3 -0
  86. package/dist/interfaces/resource-permission-repository.interface.js.map +1 -0
  87. package/dist/interfaces/tenant-extractor.interface.d.ts +4 -0
  88. package/dist/interfaces/tenant-extractor.interface.d.ts.map +1 -0
  89. package/dist/interfaces/tenant-extractor.interface.js +3 -0
  90. package/dist/interfaces/tenant-extractor.interface.js.map +1 -0
  91. package/dist/interfaces/tenant-repository.interface.d.ts +9 -0
  92. package/dist/interfaces/tenant-repository.interface.d.ts.map +1 -0
  93. package/dist/interfaces/tenant-repository.interface.js +3 -0
  94. package/dist/interfaces/tenant-repository.interface.js.map +1 -0
  95. package/dist/interfaces/user-repository.interface.d.ts +26 -18
  96. package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
  97. package/dist/repositories/noop-brute-force.repository.d.ts +1 -1
  98. package/dist/repositories/noop-brute-force.repository.d.ts.map +1 -1
  99. package/dist/repositories/noop-brute-force.repository.js +6 -6
  100. package/dist/repositories/noop-brute-force.repository.js.map +1 -1
  101. package/dist/repositories/noop-tenant-extractor.d.ts +8 -0
  102. package/dist/repositories/noop-tenant-extractor.d.ts.map +1 -0
  103. package/dist/repositories/noop-tenant-extractor.js +35 -0
  104. package/dist/repositories/noop-tenant-extractor.js.map +1 -0
  105. package/dist/repositories/noop-tenant.repository.d.ts +8 -0
  106. package/dist/repositories/noop-tenant.repository.d.ts.map +1 -0
  107. package/dist/repositories/noop-tenant.repository.js +39 -0
  108. package/dist/repositories/noop-tenant.repository.js.map +1 -0
  109. package/dist/resolvers/base-auth.resolver.d.ts +18 -5
  110. package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
  111. package/dist/resolvers/base-auth.resolver.js +59 -25
  112. package/dist/resolvers/base-auth.resolver.js.map +1 -1
  113. package/dist/resolvers/oauth.controller.d.ts +1 -1
  114. package/dist/resolvers/oauth.controller.d.ts.map +1 -1
  115. package/dist/resolvers/oauth.controller.js +3 -2
  116. package/dist/resolvers/oauth.controller.js.map +1 -1
  117. package/dist/services/auth.service.d.ts +23 -3
  118. package/dist/services/auth.service.d.ts.map +1 -1
  119. package/dist/services/auth.service.js +173 -91
  120. package/dist/services/auth.service.js.map +1 -1
  121. package/dist/services/biometric-auth.service.d.ts +0 -1
  122. package/dist/services/biometric-auth.service.d.ts.map +1 -1
  123. package/dist/services/biometric-auth.service.js +0 -6
  124. package/dist/services/biometric-auth.service.js.map +1 -1
  125. package/dist/services/brute-force-protection.service.d.ts +2 -0
  126. package/dist/services/brute-force-protection.service.d.ts.map +1 -1
  127. package/dist/services/brute-force-protection.service.js +8 -0
  128. package/dist/services/brute-force-protection.service.js.map +1 -1
  129. package/dist/services/configurable-email.service.d.ts +23 -0
  130. package/dist/services/configurable-email.service.d.ts.map +1 -0
  131. package/dist/services/configurable-email.service.js +114 -0
  132. package/dist/services/configurable-email.service.js.map +1 -0
  133. package/dist/services/default-email-template-renderer.d.ts +57 -0
  134. package/dist/services/default-email-template-renderer.d.ts.map +1 -0
  135. package/dist/services/default-email-template-renderer.js +422 -0
  136. package/dist/services/default-email-template-renderer.js.map +1 -0
  137. package/dist/services/default-jwt-payload-factory.d.ts +9 -0
  138. package/dist/services/default-jwt-payload-factory.d.ts.map +1 -0
  139. package/dist/services/default-jwt-payload-factory.js +26 -0
  140. package/dist/services/default-jwt-payload-factory.js.map +1 -0
  141. package/dist/services/header-tenant-extractor.d.ts +7 -0
  142. package/dist/services/header-tenant-extractor.d.ts.map +1 -0
  143. package/dist/services/header-tenant-extractor.js +38 -0
  144. package/dist/services/header-tenant-extractor.js.map +1 -0
  145. package/dist/services/noop-email-sender.d.ts +15 -0
  146. package/dist/services/noop-email-sender.d.ts.map +1 -0
  147. package/dist/services/noop-email-sender.js +24 -0
  148. package/dist/services/noop-email-sender.js.map +1 -0
  149. package/dist/services/noop-email.service.d.ts +1 -0
  150. package/dist/services/noop-email.service.d.ts.map +1 -1
  151. package/dist/services/noop-email.service.js +7 -2
  152. package/dist/services/noop-email.service.js.map +1 -1
  153. package/dist/services/noop-sms.service.d.ts +1 -0
  154. package/dist/services/noop-sms.service.d.ts.map +1 -1
  155. package/dist/services/noop-sms.service.js +6 -1
  156. package/dist/services/noop-sms.service.js.map +1 -1
  157. package/dist/services/oauth-linking-token.service.d.ts.map +1 -1
  158. package/dist/services/oauth-linking-token.service.js +3 -8
  159. package/dist/services/oauth-linking-token.service.js.map +1 -1
  160. package/dist/services/refresh-token.service.d.ts +1 -0
  161. package/dist/services/refresh-token.service.d.ts.map +1 -1
  162. package/dist/services/refresh-token.service.js +15 -3
  163. package/dist/services/refresh-token.service.js.map +1 -1
  164. package/dist/services/resend-email-sender.d.ts +17 -0
  165. package/dist/services/resend-email-sender.d.ts.map +1 -0
  166. package/dist/services/resend-email-sender.js +45 -0
  167. package/dist/services/resend-email-sender.js.map +1 -0
  168. package/dist/services/sendgrid-email-sender.d.ts +16 -0
  169. package/dist/services/sendgrid-email-sender.d.ts.map +1 -0
  170. package/dist/services/sendgrid-email-sender.js +75 -0
  171. package/dist/services/sendgrid-email-sender.js.map +1 -0
  172. package/dist/services/sendgrid-email.service.d.ts.map +1 -1
  173. package/dist/services/sendgrid-email.service.js.map +1 -1
  174. package/dist/services/verification.service.d.ts +7 -0
  175. package/dist/services/verification.service.d.ts.map +1 -1
  176. package/dist/services/verification.service.js +104 -116
  177. package/dist/services/verification.service.js.map +1 -1
  178. package/dist/strategies/api-key.strategy.d.ts +11 -0
  179. package/dist/strategies/api-key.strategy.d.ts.map +1 -0
  180. package/dist/strategies/api-key.strategy.js +63 -0
  181. package/dist/strategies/api-key.strategy.js.map +1 -0
  182. package/dist/strategies/jwt.strategy.d.ts +6 -2
  183. package/dist/strategies/jwt.strategy.d.ts.map +1 -1
  184. package/dist/strategies/jwt.strategy.js +30 -4
  185. package/dist/strategies/jwt.strategy.js.map +1 -1
  186. package/dist/test-utils/mock-repositories.js +1 -1
  187. package/dist/test-utils/mock-repositories.js.map +1 -1
  188. package/dist/utils/cookie-options.d.ts +18 -0
  189. package/dist/utils/cookie-options.d.ts.map +1 -0
  190. package/dist/utils/cookie-options.js +65 -0
  191. package/dist/utils/cookie-options.js.map +1 -0
  192. package/dist/utils/escape-html.d.ts +2 -0
  193. package/dist/utils/escape-html.d.ts.map +1 -0
  194. package/dist/utils/escape-html.js +12 -0
  195. package/dist/utils/escape-html.js.map +1 -0
  196. package/dist/utils/execution-context.d.ts +3 -0
  197. package/dist/utils/execution-context.d.ts.map +1 -0
  198. package/dist/utils/execution-context.js +12 -0
  199. package/dist/utils/execution-context.js.map +1 -0
  200. package/dist/utils/request-helpers.d.ts +2 -0
  201. package/dist/utils/request-helpers.d.ts.map +1 -0
  202. package/dist/utils/request-helpers.js +9 -0
  203. package/dist/utils/request-helpers.js.map +1 -0
  204. package/package.json +13 -6
  205. package/dist/resolvers/auth.resolver.d.ts +0 -73
  206. package/dist/resolvers/auth.resolver.d.ts.map +0 -1
  207. package/dist/resolvers/auth.resolver.js +0 -472
  208. package/dist/resolvers/auth.resolver.js.map +0 -1
  209. package/dist/utils/passport-inspector.d.ts +0 -11
  210. package/dist/utils/passport-inspector.d.ts.map +0 -1
  211. package/dist/utils/passport-inspector.js +0 -48
  212. 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 package for NestJS with GraphQL, extracted from the Lift fitness app.
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
- ## 🚨 BREAKING CHANGES in v0.2.0
41
+ ## Design Principles
8
42
 
9
- **Version 0.2.0 introduces a complete architectural refactor.** If upgrading from v0.1.x, you **MUST** follow the migration guide.
10
-
11
- **Key Changes**:
12
- - Package no longer exports concrete `AuthResolver` - you must create your own extending `BaseAuthResolver<T>`
13
- - All entities/DTOs are now interfaces (IAuthUser, IAuthSignupInput, etc.) - you define GraphQL types
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 @yourorg/nestjs-auth-graphql
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/config @nestjs/graphql @nestjs/jwt @nestjs/passport @nestjs/throttler graphql passport passport-jwt reflect-metadata rxjs
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
- ### 1. Implement Required Repositories
65
+ Minimal setup: JWT authentication with email/password login.
51
66
 
52
- The package uses interfaces for data persistence - you must implement these for your database (Prisma, TypeORM, etc.):
67
+ ### 1. Implement the required repositories
53
68
 
54
69
  ```typescript
55
- import { IUserRepository, IRefreshTokenRepository, IAuthUser } from '@yourorg/nestjs-auth-graphql';
70
+ // users.repository.ts
56
71
  import { Injectable } from '@nestjs/common';
57
- import { PrismaService } from './prisma.service';
72
+ import { IUserRepositoryCore, CreateUserData } from '@ambushsoftworks/nestjs-auth-graphql';
58
73
 
59
74
  @Injectable()
60
- export class PrismaUserRepository implements IUserRepository {
61
- constructor(private prisma: PrismaService) {}
62
-
63
- async findById(id: string): Promise<IAuthUser | null> {
64
- return this.prisma.user.findUnique({ where: { id } });
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
- @Injectable()
79
- export class PrismaRefreshTokenRepository implements IRefreshTokenRepository {
80
- constructor(private prisma: PrismaService) {}
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
- // Implement other required methods...
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. Configure the Auth Module
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 { AuthModule } from '@yourorg/nestjs-auth-graphql';
97
- import { ConfigModule, ConfigService } from '@nestjs/config';
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: [PrismaModule, ConfigModule],
108
- inject: [
109
- PrismaUserRepository, // Inject repository instances
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
- **Note**: You must register your repository classes in the `providers` array of the module where you call `forRootAsync()`. NestJS will inject instances of these repositories into the factory function.
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, Query } from '@nestjs/graphql';
172
- import { UseGuards } from '@nestjs/common';
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
- JwtAuthGuard,
177
- CurrentUser
178
- } from '@yourorg/nestjs-auth-graphql';
179
- import { User } from '../users/entities/user.entity'; // Your User @ObjectType
180
- import { AuthResponse } from './dto/auth-response.dto'; // Your AuthResponse @ObjectType
181
- import { LoginInput } from './dto/login.input'; // Your @InputType classes
182
- import { SignupInput } from './dto/signup.input';
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
- // Core auth mutations (required - override to provide GraphQL types)
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
- @Throttle({ default: { limit: 5, ttl: 60000 } })
197
- async signup(@Args('input') input: SignupInput): Promise<AuthResponse> {
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
- @Throttle({ default: { limit: 5, ttl: 60000 } })
203
- async login(
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
- @Throttle({ default: { limit: 10, ttl: 60000 } })
212
- async refreshToken(@Args('input') input: RefreshTokenInput): Promise<AuthResponse> {
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
- @UseGuards(JwtAuthGuard)
218
- @Throttle({ default: { limit: 10, ttl: 60000 } })
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
- **Why this pattern?**
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
- AuthResolver, // Add your resolver here
250
- PrismaUserRepository,
251
- PrismaRefreshTokenRepository,
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
- ### 4. Configure Input Validation
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 { NestFactory } from '@nestjs/core';
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
- await app.listen(3000);
284
- }
285
- bootstrap();
196
+ @Public()
197
+ @Mutation(() => AuthResponse)
198
+ async login() { /* ... */ }
286
199
  ```
287
200
 
288
- **What gets validated:**
201
+ ## Why BaseAuthResolver?
289
202
 
290
- The package validates all input fields with appropriate decorators:
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
- - **Email fields**: `@IsEmail()` - Validates proper email format
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
- **Validation error responses:**
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
- When validation fails, NestJS automatically returns a `400 Bad Request` with detailed error messages:
209
+ ### Available `perform*()` methods
304
210
 
305
- ```json
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
- **Custom validation for your DTOs:**
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
- If you create your own input DTOs implementing the package interfaces (recommended for full type control), add `class-validator` decorators:
222
+ **Verification:**
320
223
 
321
- ```typescript
322
- import { InputType, Field } from '@nestjs/graphql';
323
- import { IsEmail, IsString, MinLength, Matches } from 'class-validator';
324
- import { IAuthSignupInput } from '@ambushsoftworks/nestjs-auth-graphql';
325
-
326
- @InputType()
327
- export class SignupInput implements IAuthSignupInput {
328
- @Field()
329
- @IsEmail({}, { message: 'Please provide a valid email address' })
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
- **Note**: The package's default input classes (marked as `@deprecated`) already include validation decorators for backward compatibility. If using these, validation works automatically with `ValidationPipe` enabled.
234
+ **Password:**
343
235
 
344
- ## Configuration Options
345
-
346
- ### Required Options
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
- - `userRepositoryInstance`: Instance of `IUserRepository` implementation
349
- - `refreshTokenRepositoryInstance`: Instance of `IRefreshTokenRepository` implementation
350
- - `jwtSecret`: Secret key for signing JWT tokens
242
+ **Other:**
351
243
 
352
- ### Optional Options
244
+ | Method | Purpose |
245
+ |--------|---------|
246
+ | `performCheckAccountLockStatus(email)` | Check brute force lock status |
247
+ | `performCompleteFacebookSignUp(input)` | Facebook email fallback |
353
248
 
354
- - `emailServiceInstance`: Instance of `IEmailService` implementation (default: null)
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
- ```typescript
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
- ## Using Default Implementations
253
+ ### Cookie Authentication
384
254
 
385
- ### SendGrid Email Service
255
+ Enable `features.cookieAuth` to deliver tokens via HttpOnly cookies instead of response bodies.
386
256
 
387
257
  ```typescript
388
- import { SendGridEmailService } from '@yourorg/nestjs-auth-graphql';
389
- import { ConfigService } from '@nestjs/config';
390
-
391
- @Module({
392
- imports: [
393
- AuthModule.forRootAsync({
394
- inject: [SendGridEmailService, ConfigService, /* ... */],
395
- useFactory: (emailSvc, config, /* ... */) => ({
396
- emailServiceInstance: emailSvc,
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
- ### Twilio SMS Service
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
- ```typescript
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
- @Module({
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
- // Environment variables required:
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
- ### No-Op Services (Development/Testing)
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 { NoOpEmailService, NoOpSmsService } from '@yourorg/nestjs-auth-graphql';
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
- NoOpEmailService, // These log operations instead of sending actual emails/SMS
456
- NoOpSmsService,
289
+ { provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) },
290
+ { provide: APP_GUARD, useClass: CsrfGuard },
457
291
  ],
458
292
  })
459
293
  ```
460
294
 
461
- ## Password Reset
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
- @Entity()
499
- export class User {
500
- @PrimaryGeneratedColumn('uuid')
501
- id: string;
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
- #### Step 2: Configure Email Service
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
- Ensure `SendGridEmailService` (or your custom email service) is configured:
312
+ ### Multi-Tenancy
313
+
314
+ Enable tenant resolution and permission checking across requests.
513
315
 
514
316
  ```typescript
515
317
  AuthModule.forRootAsync({
516
- imports: [ConfigModule],
517
- inject: [ConfigService, UsersRepository, /* ... */],
518
- useFactory: (config: ConfigService, usersRepo, /* ... */) => ({
519
- // ... other options
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
- #### Step 3: Create GraphQL DTOs
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
- import { InputType, Field } from '@nestjs/graphql';
547
- import { IAuthRequestPasswordResetInput } from '@ambushsoftworks/nestjs-auth-graphql';
548
-
549
- @InputType()
550
- export class RequestPasswordResetInput implements IAuthRequestPasswordResetInput {
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
- **`reset-password.input.ts`:**
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
- @Field(() => String, { description: 'New password (8+ chars, uppercase, lowercase, number)' })
572
- newPassword: string;
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
- **`password-reset-response.dto.ts`:**
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
- @ObjectType()
582
- export class PasswordResetResponse implements IAuthPasswordResetResponse {
583
- @Field(() => Boolean)
584
- success: boolean;
356
+ ```typescript
357
+ import { HeaderTenantExtractor } from '@ambushsoftworks/nestjs-auth-graphql';
585
358
 
586
- @Field(() => String)
587
- message: string;
359
+ // Default: reads x-tenant-id header
360
+ const extractor = new HeaderTenantExtractor();
588
361
 
589
- @Field(() => Int, { nullable: true })
590
- retryAfterSeconds?: number;
591
- }
362
+ // Custom header name
363
+ const extractor = new HeaderTenantExtractor('x-org-id');
592
364
  ```
593
365
 
594
- #### Step 4: Add Resolver Mutations
595
-
596
- Extend your custom resolver with password reset mutations:
366
+ **Access tenant context** in resolvers:
597
367
 
598
368
  ```typescript
599
- import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
600
- import { Throttle } from '@nestjs/throttler';
601
- import { BaseAuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
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
- #### Step 5: Deploy & Test
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
- # Test GraphQL API
647
- curl -X POST http://localhost:3000/graphql \
648
- -H "Content-Type: application/json" \
649
- -d '{"query":"mutation { requestPasswordReset(input: {email: \"test@example.com\"}) { success message } }"}'
377
+ ```typescript
378
+ @SkipTenant()
379
+ @Query(() => User)
380
+ async me(@CurrentUser() user: User) { /* ... */ }
650
381
  ```
651
382
 
652
- ### GraphQL Schema
653
-
654
- After setup, your schema will include:
383
+ **Permission checking** with `@RequirePermissions()`:
655
384
 
656
- ```graphql
657
- type Mutation {
658
- requestPasswordReset(input: RequestPasswordResetInput!): PasswordResetResponse!
659
- resetPassword(input: ResetPasswordInput!): PasswordResetResponse!
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
- ### Error Handling
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
- try {
694
- const result = await client.mutate({
695
- mutation: RESET_PASSWORD_MUTATION,
696
- variables: { input: { email, code, newPassword } },
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
- ### Security Considerations
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
- 2. **Rate limiting is essential**
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
- export class AppAuthHooks implements IAuthLifecycleHooks<User> {
750
- async onPasswordReset(user: User): Promise<void> {
751
- // Send security alert to user's phone
752
- await this.smsService.send(user.phoneNumber, 'Your password was just changed');
753
-
754
- // Log to analytics
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
- ## GraphQL API
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
- # Refresh Token
789
- mutation RefreshToken($input: RefreshTokenInput!) {
790
- refreshToken(refreshTokenInput: $input) {
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
- # SMS Verification
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
- # Password Reset Request
820
- mutation RequestPasswordReset($input: RequestPasswordResetInput!) {
821
- requestPasswordReset(input: $input) {
822
- success
823
- message
824
- retryAfterSeconds
825
- }
826
- }
421
+ ### Email System
827
422
 
828
- # Password Reset Confirmation
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
- # Google OAuth Account Linking
837
- mutation LinkGoogleAccount($input: LinkGoogleAccountInput!) {
838
- linkGoogleAccount(linkGoogleAccountInput: $input) {
839
- user { id googleId }
840
- }
841
- }
425
+ ```typescript
426
+ import { SendGridEmailSender } from '@ambushsoftworks/nestjs-auth-graphql';
842
427
 
843
- # Biometric Enrollment
844
- mutation EnrollBiometric($input: EnrollBiometricInput!) {
845
- enrollBiometric(enrollBiometricInput: $input) {
846
- credentialId
847
- publicKey
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
- ### Queries
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
- # Check account lock status
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
- # Get biometric auth status
876
- query BiometricStatus {
877
- biometricStatus {
878
- isEnabled
879
- registeredDevices { deviceId deviceName }
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
- ## Implementing Custom Lifecycle Hooks
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
- import { Injectable } from '@nestjs/common';
890
- import { IAuthLifecycleHooks, IAuthUser } from '@yourorg/nestjs-auth-graphql';
891
-
892
- @Injectable()
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
- ## Security Features
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
- ## Security Best Practices
478
+ Two modes for email verification and password reset:
972
479
 
973
- ### 🔐 Secret Management
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
- // BAD: Hard-coded secrets
1017
- AuthModule.forRootAsync({
1018
- useFactory: () => ({
1019
- jwtSecret: 'my-secret-key', // NEVER DO THIS
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
- ### 🛡️ Token Configuration
492
+ ### OAuth
1049
493
 
1050
- **Access Token Best Practices:**
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
- // Short-lived access tokens (minimize damage if compromised)
1056
- jwtExpiresIn: '15m', // Default: 15 minutes
1057
-
1058
- // For mobile apps with poor connectivity, consider 1h max
1059
- // jwtExpiresIn: '1h', // Longer for mobile, but less secure
1060
-
1061
- // NEVER use long-lived access tokens
1062
- // jwtExpiresIn: '30d', // ❌ INSECURE
1063
- }),
1064
- });
1065
- ```
1066
-
1067
- **Refresh Token Best Practices:**
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
- **Token Rotation**: This package automatically rotates refresh tokens on each use. Old tokens are invalidated to prevent replay attacks.
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
- ### 🔒 Password Policy
519
+ ### Brute Force Protection
1087
520
 
1088
- **Enforce Strong Passwords:**
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
- The package includes default password validation (8+ characters, uppercase, lowercase, number, special character). For stronger policies:
523
+ ### Password Policy
1091
524
 
1092
525
  ```typescript
1093
- import { InputType, Field } from '@nestjs/graphql';
1094
- import { IsEmail, IsString, MinLength, MaxLength, Matches } from 'class-validator';
1095
-
1096
- @InputType()
1097
- export class SignupInput {
1098
- @Field()
1099
- @IsEmail({}, { message: 'Please provide a valid email address' })
1100
- email: string;
1101
-
1102
- @Field()
1103
- @IsString()
1104
- @MinLength(12, { message: 'Password must be at least 12 characters' })
1105
- @MaxLength(128, { message: 'Password must not exceed 128 characters' })
1106
- @Matches(
1107
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/,
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
- **Additional Recommendations:**
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
- **Configure Throttling:**
546
+ React to auth events without modifying core logic:
1123
547
 
1124
548
  ```typescript
1125
- import { ThrottlerModule } from '@nestjs/throttler';
1126
-
1127
- @Module({
1128
- imports: [
1129
- // Global rate limiting
1130
- ThrottlerModule.forRoot([{
1131
- ttl: 60000, // 60 seconds
1132
- limit: 100, // 100 requests per minute
1133
- }]),
1134
-
1135
- AuthModule.forRootAsync({
1136
- // Auth-specific throttling is handled per-mutation
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
- **Per-Endpoint Throttling** (already implemented in BaseAuthResolver):
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
- ### 🌐 HTTPS/TLS Requirements
565
+ ### JWT Validation Modes
1152
566
 
1153
- **PRODUCTION REQUIREMENT**: All authentication endpoints MUST use HTTPS.
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
- // Production enforcement example
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
- **OAuth Callback URLs**: Must be HTTPS in production (required by Google/Facebook).
574
+ ### JWT Payload Factory
1179
575
 
1180
- ### 📊 Security Logging
1181
-
1182
- **Implement Custom Security Logger** for production monitoring:
576
+ Customize JWT claims by providing an `IJwtPayloadFactory`:
1183
577
 
1184
578
  ```typescript
1185
- import { Injectable } from '@nestjs/common';
1186
- import { IAuthLogger, SecurityEvent } from '@ambushsoftworks/nestjs-auth-graphql';
1187
-
1188
- @Injectable()
1189
- export class ProductionAuthLogger implements IAuthLogger {
1190
- constructor(
1191
- private readonly datadogClient: DatadogClient,
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
- **Critical Events to Monitor:**
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
- **Environment Separation:**
591
+ ## Configuration Reference
1243
592
 
1244
- ```bash
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
- **Multi-Instance Deployment** (Load Balanced):
1262
- - See "Production Deployment" section in CLAUDE.md for cache limitations
1263
- - Use Redis for shared refresh token cache across instances
1264
- - OR configure sticky sessions on load balancer
1265
- - OR accept 10-second grace period limitation for most apps
1266
-
1267
- **Security Checklist:**
1268
- - ✅ HTTPS enforced (no HTTP in production)
1269
- - Secrets managed via environment variables or secret manager
1270
- - ✅ CORS configured to allow only trusted domains
1271
- - Rate limiting enabled globally
1272
- - Security logging to centralized service
1273
- - Database connections use TLS
1274
- - OAuth callback URLs whitelisted
1275
- - ValidationPipe enabled globally
1276
- - Helmet middleware for HTTP security headers
1277
- - CSRF protection enabled (built-in for OAuth)
1278
-
1279
- **Security Headers Example:**
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
- ```typescript
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
- await app.listen(3000);
1304
- }
1305
- ```
701
+ ## Decorators
1306
702
 
1307
- ### 🔍 Security Audits
1308
-
1309
- **Regular Security Practices:**
1310
- 1. **Dependency Scanning**: `npm audit` (weekly in CI/CD)
1311
- 2. **Secret Scanning**: Use tools like GitGuardian, TruffleHog
1312
- 3. **Penetration Testing**: Quarterly security assessments
1313
- 4. **Log Review**: Weekly review of security event logs
1314
- 5. **Incident Response**: Document and practice breach response procedures
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
- #### RefreshToken Table
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
- #### EmailVerification Table
1371
-
1372
- ```sql
1373
- CREATE TABLE email_verifications (
1374
- id VARCHAR(36) PRIMARY KEY,
1375
- userId VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
1376
- codeHash VARCHAR(255) NOT NULL, -- HMAC-SHA256 hash of 6-digit code
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
- #### PhoneVerification Table
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
- #### FailedLoginAttempt Table
1403
-
1404
- ```sql
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
- #### BiometricCredential Table
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
- #### BiometricChallenge Table
1432
-
1433
- ```sql
1434
- CREATE TABLE biometric_challenges (
1435
- id VARCHAR(36) PRIMARY KEY,
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
- ### Option 2: Adapter Pattern (Flexible Schema)
739
+ ## Utilities
1447
740
 
1448
- If your database schema differs from the package expectations, create **adapter repositories** that translate between your schema and the package interfaces.
741
+ Helper functions exported for use in custom guards and middleware:
1449
742
 
1450
- **Example**: Lift app uses a unified `VerificationCode` table for both email and SMS verification:
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 PrismaVerificationRepository implements IVerificationRepository {
1455
- constructor(private prisma: PrismaService) {}
1456
-
1457
- async storeVerificationCode(
1458
- userId: string,
1459
- code: string,
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
- **Trade-offs**:
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
- 1. Replace `import { AuthModule } from './auth/auth.module'` with `import { AuthModule } from '@yourorg/nestjs-auth-graphql'`
1550
- 2. Implement `IUserRepository` and `IRefreshTokenRepository` using your Prisma models
1551
- 3. Configure `AuthModule.forRootAsync()` with your repositories and services
1552
- 4. Remove old `src/auth/` directory (keep only repository implementations)
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).