@ambushsoftworks/nestjs-auth-graphql 0.6.5 → 0.7.2

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 (91) hide show
  1. package/README.md +590 -1291
  2. package/dist/auth.module.d.ts +7 -3
  3. package/dist/auth.module.d.ts.map +1 -1
  4. package/dist/auth.module.js +13 -0
  5. package/dist/auth.module.js.map +1 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +2 -1
  9. package/dist/constants.js.map +1 -1
  10. package/dist/decorators/current-realm.decorator.d.ts +2 -0
  11. package/dist/decorators/current-realm.decorator.d.ts.map +1 -0
  12. package/dist/decorators/current-realm.decorator.js +10 -0
  13. package/dist/decorators/current-realm.decorator.js.map +1 -0
  14. package/dist/dto/jwt-payload.interface.d.ts +1 -0
  15. package/dist/dto/jwt-payload.interface.d.ts.map +1 -1
  16. package/dist/guards/csrf.guard.d.ts +0 -1
  17. package/dist/guards/csrf.guard.d.ts.map +1 -1
  18. package/dist/guards/csrf.guard.js +2 -8
  19. package/dist/guards/csrf.guard.js.map +1 -1
  20. package/dist/guards/permission.guard.d.ts +0 -1
  21. package/dist/guards/permission.guard.d.ts.map +1 -1
  22. package/dist/guards/permission.guard.js +2 -8
  23. package/dist/guards/permission.guard.js.map +1 -1
  24. package/dist/guards/tenant.guard.d.ts +0 -1
  25. package/dist/guards/tenant.guard.d.ts.map +1 -1
  26. package/dist/guards/tenant.guard.js +2 -9
  27. package/dist/guards/tenant.guard.js.map +1 -1
  28. package/dist/index.d.ts +5 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +5 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/interfaces/jwt-payload-factory.interface.d.ts +1 -0
  33. package/dist/interfaces/jwt-payload-factory.interface.d.ts.map +1 -1
  34. package/dist/interfaces/realm-extractor.interface.d.ts +4 -0
  35. package/dist/interfaces/realm-extractor.interface.d.ts.map +1 -0
  36. package/dist/interfaces/realm-extractor.interface.js +3 -0
  37. package/dist/interfaces/realm-extractor.interface.js.map +1 -0
  38. package/dist/interfaces/user-repository.interface.d.ts +6 -5
  39. package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
  40. package/dist/middleware/realm.middleware.d.ts +8 -0
  41. package/dist/middleware/realm.middleware.d.ts.map +1 -0
  42. package/dist/middleware/realm.middleware.js +41 -0
  43. package/dist/middleware/realm.middleware.js.map +1 -0
  44. package/dist/resolvers/base-auth.resolver.d.ts +12 -3
  45. package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
  46. package/dist/resolvers/base-auth.resolver.js +63 -74
  47. package/dist/resolvers/base-auth.resolver.js.map +1 -1
  48. package/dist/resolvers/oauth.controller.d.ts +1 -1
  49. package/dist/resolvers/oauth.controller.d.ts.map +1 -1
  50. package/dist/resolvers/oauth.controller.js +10 -6
  51. package/dist/resolvers/oauth.controller.js.map +1 -1
  52. package/dist/services/auth.service.d.ts +17 -13
  53. package/dist/services/auth.service.d.ts.map +1 -1
  54. package/dist/services/auth.service.js +70 -111
  55. package/dist/services/auth.service.js.map +1 -1
  56. package/dist/services/biometric-auth.service.d.ts +0 -1
  57. package/dist/services/biometric-auth.service.d.ts.map +1 -1
  58. package/dist/services/biometric-auth.service.js +0 -6
  59. package/dist/services/biometric-auth.service.js.map +1 -1
  60. package/dist/services/brute-force-protection.service.d.ts +4 -4
  61. package/dist/services/brute-force-protection.service.d.ts.map +1 -1
  62. package/dist/services/brute-force-protection.service.js +8 -8
  63. package/dist/services/brute-force-protection.service.js.map +1 -1
  64. package/dist/services/default-jwt-payload-factory.d.ts +1 -0
  65. package/dist/services/default-jwt-payload-factory.d.ts.map +1 -1
  66. package/dist/services/default-jwt-payload-factory.js +5 -1
  67. package/dist/services/default-jwt-payload-factory.js.map +1 -1
  68. package/dist/services/oauth-linking-token.service.d.ts.map +1 -1
  69. package/dist/services/oauth-linking-token.service.js +3 -8
  70. package/dist/services/oauth-linking-token.service.js.map +1 -1
  71. package/dist/services/verification.service.d.ts +9 -7
  72. package/dist/services/verification.service.d.ts.map +1 -1
  73. package/dist/services/verification.service.js +36 -131
  74. package/dist/services/verification.service.js.map +1 -1
  75. package/dist/strategies/api-key.strategy.d.ts +1 -1
  76. package/dist/strategies/api-key.strategy.d.ts.map +1 -1
  77. package/dist/strategies/api-key.strategy.js +3 -6
  78. package/dist/strategies/api-key.strategy.js.map +1 -1
  79. package/dist/strategies/jwt.strategy.d.ts +3 -1
  80. package/dist/strategies/jwt.strategy.d.ts.map +1 -1
  81. package/dist/strategies/jwt.strategy.js +10 -1
  82. package/dist/strategies/jwt.strategy.js.map +1 -1
  83. package/dist/utils/execution-context.d.ts +3 -0
  84. package/dist/utils/execution-context.d.ts.map +1 -0
  85. package/dist/utils/execution-context.js +12 -0
  86. package/dist/utils/execution-context.js.map +1 -0
  87. package/dist/utils/request-helpers.d.ts +2 -0
  88. package/dist/utils/request-helpers.d.ts.map +1 -0
  89. package/dist/utils/request-helpers.js +9 -0
  90. package/dist/utils/request-helpers.js.map +1 -0
  91. package/package.json +1 -1
package/README.md CHANGED
@@ -1,1560 +1,859 @@
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, realm-based identity isolation, 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
+ - [Realm-Based Identity Isolation](#realm-based-identity-isolation)
15
+ - [API Key Authentication](#api-key-authentication)
16
+ - [Email System](#email-system)
17
+ - [Verification Modes](#verification-modes)
18
+ - [OAuth](#oauth)
19
+ - [Brute Force Protection](#brute-force-protection)
20
+ - [Password Policy](#password-policy)
21
+ - [Lifecycle Hooks](#lifecycle-hooks)
22
+ - [JWT Validation Modes](#jwt-validation-modes)
23
+ - [JWT Payload Factory](#jwt-payload-factory)
24
+ - [Configuration Reference](#configuration-reference)
25
+ - [Required Options](#required-options)
26
+ - [Common Options](#common-options)
27
+ - [Feature Flags](#feature-flags-features)
28
+ - [Security Secrets](#security-secrets)
29
+ - [Cookie Options](#cookie-options-cookie)
30
+ - [CSRF Options](#csrf-options-csrf)
31
+ - [Composable Email](#composable-email-email)
32
+ - [Verification Options](#verification-options-verification)
33
+ - [Optional Instance Options](#optional-instance-options)
34
+ - [Decorators](#decorators)
35
+ - [Guards](#guards)
36
+ - [Utilities](#utilities)
37
+ - [Security Features](#security-features)
38
+ - [License](#license)
4
39
 
5
40
  ---
6
41
 
7
- ## 🚨 BREAKING CHANGES in v0.2.0
42
+ ## Design Principles
8
43
 
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
44
+ - **Interface-driven persistence** -- All storage is behind interfaces (`IUserRepository`, `IRefreshTokenRepository`, `ITenantRepository`, etc.). Bring your own database.
45
+ - **Instance-based DI** -- Repositories and services are injected as instances via `useFactory`, not as classes.
46
+ - **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)).
47
+ - **Guards are exported, not auto-registered** -- You register guards in your own `APP_GUARD` chain to control execution order.
48
+ - **NoOp fallbacks** -- Every optional dependency has a no-op implementation, so the module works with minimal config.
35
49
 
36
50
  ## Installation
37
51
 
38
52
  ```bash
39
- npm install @yourorg/nestjs-auth-graphql
53
+ npm install @ambushsoftworks/nestjs-auth-graphql
40
54
  ```
41
55
 
42
56
  ### Peer Dependencies
43
57
 
44
58
  ```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
59
+ npm install @nestjs/common @nestjs/core @nestjs/graphql @nestjs/jwt @nestjs/passport @nestjs/throttler graphql passport passport-jwt reflect-metadata rxjs
46
60
  ```
47
61
 
62
+ `resend` is an optional peer dependency -- install it only if using the Resend email sender.
63
+
48
64
  ## Quick Start
49
65
 
50
- ### 1. Implement Required Repositories
66
+ Minimal setup: JWT authentication with email/password login.
51
67
 
52
- The package uses interfaces for data persistence - you must implement these for your database (Prisma, TypeORM, etc.):
68
+ ### 1. Implement the required repositories
53
69
 
54
70
  ```typescript
55
- import { IUserRepository, IRefreshTokenRepository, IAuthUser } from '@yourorg/nestjs-auth-graphql';
71
+ // users.repository.ts
56
72
  import { Injectable } from '@nestjs/common';
57
- import { PrismaService } from './prisma.service';
73
+ import { IUserRepositoryCore, CreateUserData } from '@ambushsoftworks/nestjs-auth-graphql';
58
74
 
59
75
  @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...
76
+ export class UsersRepository implements IUserRepositoryCore<User> {
77
+ async findByEmail(email: string): Promise<User | null> { /* ... */ }
78
+ async findById(id: string): Promise<User | null> { /* ... */ }
79
+ async create(data: CreateUserData): Promise<User> { /* ... */ }
80
+ // ... implement remaining IUserRepositoryCore methods
76
81
  }
82
+ ```
77
83
 
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
- }
84
+ ```typescript
85
+ // refresh-token.repository.ts
86
+ import { Injectable } from '@nestjs/common';
87
+ import { IRefreshTokenRepository } from '@ambushsoftworks/nestjs-auth-graphql';
85
88
 
86
- // Implement other required methods...
89
+ @Injectable()
90
+ export class RefreshTokenRepository implements IRefreshTokenRepository {
91
+ async create(data: {
92
+ userId: string;
93
+ token: string;
94
+ hashedToken: string;
95
+ expiresAt: Date;
96
+ deviceInfo?: string | null;
97
+ }): Promise<any> { /* ... */ }
98
+ async findByHashedToken(hashedToken: string): Promise<any | null> { /* ... */ }
99
+ async deleteByUserId(userId: string): Promise<number> { /* ... */ }
100
+ // ... implement remaining methods
87
101
  }
88
102
  ```
89
103
 
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.
104
+ ### 2. Register the module
93
105
 
94
106
  ```typescript
95
107
  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';
108
+ import { ConfigService } from '@nestjs/config';
109
+ import { AuthModule } from '@ambushsoftworks/nestjs-auth-graphql';
101
110
 
102
111
  @Module({
103
112
  imports: [
104
- ConfigModule.forRoot(),
105
- PrismaModule, // Import module that provides repositories
106
113
  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)
114
+ imports: [ConfigModule, DatabaseModule],
115
+ inject: [UsersRepository, RefreshTokenRepository, ConfigService],
116
+ useFactory: (usersRepo, tokenRepo, config) => ({
119
117
  userRepositoryInstance: usersRepo,
120
118
  refreshTokenRepositoryInstance: tokenRepo,
121
-
122
- // JWT configuration (required)
123
119
  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
120
  }),
151
121
  }),
152
122
  ],
153
- providers: [
154
- // Register repository classes so NestJS can inject them
155
- PrismaUserRepository,
156
- PrismaRefreshTokenRepository,
157
- ],
123
+ providers: [UsersRepository, RefreshTokenRepository],
158
124
  })
159
125
  export class AppModule {}
160
126
  ```
161
127
 
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`:
128
+ ### 3. Create your auth resolver
169
129
 
170
130
  ```typescript
171
- import { Resolver, Mutation, Args, Context, Query } from '@nestjs/graphql';
172
- import { UseGuards } from '@nestjs/common';
173
- import { Throttle } from '@nestjs/throttler';
131
+ import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
132
+ import { Inject } from '@nestjs/common';
174
133
  import {
175
134
  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
- */
135
+ CurrentUser,
136
+ AuthService,
137
+ BruteForceProtectionService,
138
+ USER_REPOSITORY,
139
+ AUTH_LOGGER,
140
+ } from '@ambushsoftworks/nestjs-auth-graphql';
141
+
191
142
  @Resolver()
192
143
  export class AuthResolver extends BaseAuthResolver<User> {
193
- // Core auth mutations (required - override to provide GraphQL types)
144
+ constructor(
145
+ authService: AuthService,
146
+ bruteForceProtection: BruteForceProtectionService,
147
+ @Inject(USER_REPOSITORY) userRepository: any,
148
+ @Inject(AUTH_LOGGER) authLogger: any,
149
+ ) {
150
+ super(authService, bruteForceProtection, userRepository, authLogger);
151
+ }
194
152
 
195
153
  @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>;
154
+ async signup(@Args('input') input: SignupInput, @Context() ctx: any) {
155
+ return this.performSignup(input, ctx);
199
156
  }
200
157
 
201
158
  @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>;
159
+ async login(@Args('input') input: LoginInput, @Context() ctx: any) {
160
+ return this.performLogin(input, ctx);
208
161
  }
209
162
 
210
163
  @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>;
164
+ async refreshToken(@Args('input') input: RefreshTokenInput, @Context() ctx: any) {
165
+ return this.performRefreshToken(input, ctx);
214
166
  }
215
167
 
216
168
  @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>;
224
- }
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>;
169
+ async logout(@Args('input') input: LogoutInput, @CurrentUser() user: User) {
170
+ return this.performLogout(input, user);
231
171
  }
232
-
233
- // Add overrides for other mutations: verifyEmail, sendPhoneVerification, etc.
234
- // See full example in Lift backend: src/auth/lift-auth.resolver.ts
235
172
  }
236
173
  ```
237
174
 
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:**
175
+ ### 4. Set up the guard chain
244
176
 
245
177
  ```typescript
178
+ import { APP_GUARD } from '@nestjs/core';
179
+ import { createAuthGuard } from '@ambushsoftworks/nestjs-auth-graphql';
180
+
246
181
  @Module({
247
- imports: [AuthModule.forRootAsync(...)],
248
182
  providers: [
249
- AuthResolver, // Add your resolver here
250
- PrismaUserRepository,
251
- PrismaRefreshTokenRepository,
183
+ {
184
+ provide: APP_GUARD,
185
+ useClass: createAuthGuard(['jwt'], { allowPublic: true }),
186
+ },
252
187
  ],
253
188
  })
254
189
  export class AppModule {}
255
190
  ```
256
191
 
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`:
192
+ Mark public routes with `@Public()`:
262
193
 
263
194
  ```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
- );
195
+ import { Public } from '@ambushsoftworks/nestjs-auth-graphql';
282
196
 
283
- await app.listen(3000);
284
- }
285
- bootstrap();
197
+ @Public()
198
+ @Mutation(() => AuthResponse)
199
+ async login() { /* ... */ }
286
200
  ```
287
201
 
288
- **What gets validated:**
202
+ ## Why BaseAuthResolver?
289
203
 
290
- The package validates all input fields with appropriate decorators:
204
+ 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
205
 
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
206
+ `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
207
 
301
- **Validation error responses:**
208
+ 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
209
 
303
- When validation fails, NestJS automatically returns a `400 Bad Request` with detailed error messages:
210
+ ### Available `perform*()` methods
304
211
 
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
- ```
212
+ **Authentication:**
316
213
 
317
- **Custom validation for your DTOs:**
214
+ | Method | Purpose |
215
+ |--------|---------|
216
+ | `performSignup(input, context?)` | Create account |
217
+ | `performLogin(input, context)` | Authenticate |
218
+ | `performRefreshToken(input, context?)` | Rotate tokens |
219
+ | `performLogout(input, user, context?)` | Invalidate token |
220
+ | `performLogoutAll(user, context?)` | Invalidate all tokens |
221
+ | `performGetCurrentUser(userId)` | Get current user |
318
222
 
319
- If you create your own input DTOs implementing the package interfaces (recommended for full type control), add `class-validator` decorators:
223
+ **Verification:**
320
224
 
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
- ```
225
+ | Method | Purpose |
226
+ |--------|---------|
227
+ | `performVerifyEmail(input, context?)` | Email verification |
228
+ | `performResendVerificationEmail(email)` | Resend verification email |
229
+ | `performSendPhoneVerification(input, user)` | Send SMS verification code |
230
+ | `performVerifyPhone(input, user)` | Verify phone with SMS code |
231
+ | `performResendPhoneVerification(phoneNumber, user)` | Resend phone verification |
232
+ | `performRemovePhoneNumber(user)` | Remove phone number from account |
233
+ | `performPhoneVerificationStatus(user)` | Get phone verification status |
341
234
 
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.
235
+ **Password:**
343
236
 
344
- ## Configuration Options
237
+ | Method | Purpose |
238
+ |--------|---------|
239
+ | `performRequestPasswordReset(input, context?)` | Send reset code/token |
240
+ | `performResetPassword(input, context?)` | Reset password |
241
+ | `performChangePassword(userId, current, new)` | Change password |
345
242
 
346
- ### Required Options
243
+ **Other:**
347
244
 
348
- - `userRepositoryInstance`: Instance of `IUserRepository` implementation
349
- - `refreshTokenRepositoryInstance`: Instance of `IRefreshTokenRepository` implementation
350
- - `jwtSecret`: Secret key for signing JWT tokens
245
+ | Method | Purpose |
246
+ |--------|---------|
247
+ | `performCheckAccountLockStatus(email)` | Check brute force lock status |
248
+ | `performCompleteFacebookSignUp(input)` | Facebook email fallback |
351
249
 
352
- ### Optional Options
353
-
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:
250
+ ---
371
251
 
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
- ```
252
+ ## Features
382
253
 
383
- ## Using Default Implementations
254
+ ### Cookie Authentication
384
255
 
385
- ### SendGrid Email Service
256
+ Enable `features.cookieAuth` to deliver tokens via HttpOnly cookies instead of response bodies.
386
257
 
387
258
  ```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
- ],
259
+ AuthModule.forRootAsync({
260
+ useFactory: () => ({
261
+ // ...required options
262
+ features: { cookieAuth: true },
263
+ cookie: {
264
+ httpOnly: true, // default
265
+ secure: true, // default
266
+ sameSite: 'lax', // default
267
+ domain: undefined, // browser uses request domain
268
+ useHostPrefix: true, // prefix names with __Host- for enhanced security
269
+ },
270
+ }),
404
271
  })
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
272
  ```
411
273
 
412
- ### Twilio SMS Service
274
+ 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
275
 
414
- ```typescript
415
- import { TwilioSmsService } from '@yourorg/nestjs-auth-graphql';
276
+ You can also customize cookie names and max ages -- see [Cookie Options](#cookie-options-cookie).
416
277
 
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
- })
278
+ The JWT strategy automatically reads from cookies first, then falls back to `Authorization: Bearer` headers.
431
279
 
432
- // Environment variables required:
433
- // TWILIO_ACCOUNT_SID=your_account_sid
434
- // TWILIO_AUTH_TOKEN=your_auth_token
435
- // TWILIO_PHONE_NUMBER=+1234567890
436
- ```
280
+ #### CSRF Protection
437
281
 
438
- ### No-Op Services (Development/Testing)
282
+ 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
283
 
440
284
  ```typescript
441
- import { NoOpEmailService, NoOpSmsService } from '@yourorg/nestjs-auth-graphql';
285
+ import { APP_GUARD } from '@nestjs/core';
286
+ import { createAuthGuard, CsrfGuard } from '@ambushsoftworks/nestjs-auth-graphql';
442
287
 
443
288
  @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
289
  providers: [
455
- NoOpEmailService, // These log operations instead of sending actual emails/SMS
456
- NoOpSmsService,
290
+ { provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) },
291
+ { provide: APP_GUARD, useClass: CsrfGuard },
457
292
  ],
458
293
  })
459
294
  ```
460
295
 
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
296
+ Configure via `csrf` options:
475
297
 
476
- ### Consumer Setup
477
-
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
298
  ```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
299
+ csrf: {
300
+ headerName: 'X-Requested-With', // default
301
+ requireInProduction: true, // default
302
+ exemptOperations: ['refreshToken'], // skip CSRF for specific operations
507
303
  }
508
304
  ```
509
305
 
510
- #### Step 2: Configure Email Service
306
+ The guard automatically skips when:
307
+ - The request uses `Authorization` header (Bearer/API key)
308
+ - The route has `@Public()`
309
+ - Cookie auth is not enabled
310
+ - `requireInProduction` is false and not in production
311
+ - The GraphQL operation is in `exemptOperations`
511
312
 
512
- Ensure `SendGridEmailService` (or your custom email service) is configured:
313
+ ### Multi-Tenancy
314
+
315
+ Enable tenant resolution and permission checking across requests.
513
316
 
514
317
  ```typescript
515
318
  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
- },
319
+ useFactory: (tenantRepo, tenantExtractor) => ({
320
+ // ...required options
321
+ features: { multiTenancy: true },
322
+ tenantRepositoryInstance: tenantRepo,
323
+ tenantExtractorInstance: tenantExtractor,
531
324
  }),
532
- }),
533
- ```
534
-
535
- **Environment Variable:**
536
- ```bash
537
- FRONTEND_URL=https://yourapp.com # Used in email branding
325
+ })
538
326
  ```
539
327
 
540
- #### Step 3: Create GraphQL DTOs
328
+ **Guard chain** (order matters):
541
329
 
542
- Create consumer-specific DTOs with GraphQL decorators:
543
-
544
- **`request-password-reset.input.ts`:**
545
330
  ```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
- }
331
+ providers: [
332
+ { provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) },
333
+ { provide: APP_GUARD, useClass: TenantGuard },
334
+ { provide: APP_GUARD, useClass: PermissionGuard },
335
+ ]
556
336
  ```
557
337
 
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;
338
+ **`ITenantRepository`** resolves whether a user has access to a tenant:
570
339
 
571
- @Field(() => String, { description: 'New password (8+ chars, uppercase, lowercase, number)' })
572
- newPassword: string;
573
- }
574
- ```
575
-
576
- **`password-reset-response.dto.ts`:**
577
340
  ```typescript
578
- import { ObjectType, Field, Int } from '@nestjs/graphql';
579
- import { IAuthPasswordResetResponse } from '@ambushsoftworks/nestjs-auth-graphql';
580
-
581
- @ObjectType()
582
- export class PasswordResetResponse implements IAuthPasswordResetResponse {
583
- @Field(() => Boolean)
584
- success: boolean;
585
-
586
- @Field(() => String)
587
- message: string;
588
-
589
- @Field(() => Int, { nullable: true })
590
- retryAfterSeconds?: number;
591
- }
592
- ```
593
-
594
- #### Step 4: Add Resolver Mutations
595
-
596
- Extend your custom resolver with password reset mutations:
597
-
598
- ```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>;
341
+ @Injectable()
342
+ class TenantRepository implements ITenantRepository {
343
+ async resolveTenant(userId: string, tenantId: string): Promise<ITenantContext | null> {
344
+ const membership = await this.db.membership.find({ userId, tenantId });
345
+ if (!membership) return null;
346
+ return {
347
+ tenantId,
348
+ permissions: membership.role.permissions,
349
+ metadata: { accountId: membership.accountId },
350
+ };
633
351
  }
634
352
  }
635
353
  ```
636
354
 
637
- #### Step 5: Deploy & Test
355
+ **`ITenantExtractor`** pulls the tenant ID from the request. Use the built-in `HeaderTenantExtractor` (reads `x-tenant-id` header) or implement your own:
638
356
 
639
- ```bash
640
- # Run migration
641
- npx prisma migrate deploy
357
+ ```typescript
358
+ import { HeaderTenantExtractor } from '@ambushsoftworks/nestjs-auth-graphql';
642
359
 
643
- # Start dev server
644
- npm run start:dev
360
+ // Default: reads x-tenant-id header
361
+ const extractor = new HeaderTenantExtractor();
645
362
 
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 } }"}'
363
+ // Custom header name
364
+ const extractor = new HeaderTenantExtractor('x-org-id');
650
365
  ```
651
366
 
652
- ### GraphQL Schema
653
-
654
- After setup, your schema will include:
367
+ **Access tenant context** in resolvers:
655
368
 
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
369
+ ```typescript
370
+ @Query(() => [Item])
371
+ async items(@CurrentTenant() tenant: ITenantContext) {
372
+ return this.service.findByTenant(tenant.tenantId);
676
373
  }
677
374
  ```
678
375
 
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):**
376
+ **Skip tenant resolution** for routes that don't need it:
691
377
 
692
378
  ```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
- }
379
+ @SkipTenant()
380
+ @Query(() => User)
381
+ async me(@CurrentUser() user: User) { /* ... */ }
716
382
  ```
717
383
 
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
723
-
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:
384
+ **Permission checking** with `@RequirePermissions()`:
747
385
 
748
386
  ```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
- }
387
+ @RequirePermissions('clients:read')
388
+ @Query(() => [Client])
389
+ async clients(@CurrentTenant() tenant: ITenantContext) { /* ... */ }
761
390
  ```
762
391
 
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
- }
392
+ For resource-scoped permissions, combine with `@ResourceScope()` and provide an `IResourcePermissionRepository`:
778
393
 
779
- # Login
780
- mutation Login($input: LoginInput!) {
781
- login(loginInput: $input) {
782
- accessToken
783
- refreshToken
784
- user { id email }
785
- }
786
- }
787
-
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
- }
810
-
811
- # SMS Verification
812
- mutation VerifyPhone($input: VerifyPhoneInput!) {
813
- verifyPhone(verifyPhoneInput: $input) {
814
- success
815
- user { id phoneNumber phoneVerified }
816
- }
817
- }
818
-
819
- # Password Reset Request
820
- mutation RequestPasswordReset($input: RequestPasswordResetInput!) {
821
- requestPasswordReset(input: $input) {
822
- success
823
- message
824
- retryAfterSeconds
825
- }
826
- }
827
-
828
- # Password Reset Confirmation
829
- mutation ResetPassword($input: ResetPasswordInput!) {
830
- resetPassword(input: $input) {
831
- success
832
- message
833
- }
834
- }
835
-
836
- # Google OAuth Account Linking
837
- mutation LinkGoogleAccount($input: LinkGoogleAccountInput!) {
838
- linkGoogleAccount(linkGoogleAccountInput: $input) {
839
- user { id googleId }
840
- }
841
- }
842
-
843
- # Biometric Enrollment
844
- mutation EnrollBiometric($input: EnrollBiometricInput!) {
845
- enrollBiometric(enrollBiometricInput: $input) {
846
- credentialId
847
- publicKey
848
- }
849
- }
394
+ ```typescript
395
+ @RequirePermissions('clients:write')
396
+ @ResourceScope('client', 'clientId')
397
+ @Mutation(() => Client)
398
+ async updateClient(@Args('clientId') clientId: string) { /* ... */ }
850
399
  ```
851
400
 
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
- }
401
+ ### Realm-Based Identity Isolation
866
402
 
867
- # Check account lock status
868
- query CheckAccountLockStatus($email: String!) {
869
- checkAccountLockStatus(email: $email) {
870
- isLocked
871
- remainingLockoutTime
872
- }
873
- }
403
+ Realms partition the user identity space so that users in one realm are completely invisible to another. The same email address can exist independently in different realms, each with its own password, tokens, and verification state. This is useful for white-label platforms, multi-brand businesses, and franchise systems.
874
404
 
875
- # Get biometric auth status
876
- query BiometricStatus {
877
- biometricStatus {
878
- isEnabled
879
- registeredDevices { deviceId deviceName }
880
- }
881
- }
882
- ```
883
-
884
- ## Implementing Custom Lifecycle Hooks
405
+ Realms are opt-in: provide a `realmExtractorInstance` in `AuthModuleOptions`. No separate feature flag is needed. When not provided, everything works exactly as before.
885
406
 
886
- Add custom business logic to authentication events:
407
+ **Implement `IRealmExtractor`:**
887
408
 
888
409
  ```typescript
889
410
  import { Injectable } from '@nestjs/common';
890
- import { IAuthLifecycleHooks, IAuthUser } from '@yourorg/nestjs-auth-graphql';
411
+ import { IRealmExtractor } from '@ambushsoftworks/nestjs-auth-graphql';
891
412
 
892
413
  @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}`);
414
+ export class HostnameRealmExtractor implements IRealmExtractor {
415
+ extractRealm(request: Record<string, any>): string | null {
416
+ const host = request.headers?.host;
417
+ if (!host) return null;
418
+ // e.g., "acme.example.com" -> "acme"
419
+ const subdomain = host.split('.')[0];
420
+ return subdomain || null;
902
421
  }
422
+ }
423
+ ```
903
424
 
904
- async onLogout(user: IAuthUser): Promise<void> {
905
- // Clean up sessions, log analytics, etc.
906
- console.log(`User logged out: ${user.email}`);
907
- }
425
+ **Register the module with realm support:**
908
426
 
909
- async onEmailVerified(user: IAuthUser): Promise<void> {
910
- // Send confirmation email, unlock features, etc.
911
- console.log(`Email verified: ${user.email}`);
912
- }
427
+ ```typescript
428
+ AuthModule.forRootAsync({
429
+ imports: [ConfigModule, DatabaseModule],
430
+ inject: [UsersRepository, RefreshTokenRepository, HostnameRealmExtractor, ConfigService],
431
+ useFactory: (usersRepo, tokenRepo, realmExtractor, config) => ({
432
+ userRepositoryInstance: usersRepo,
433
+ refreshTokenRepositoryInstance: tokenRepo,
434
+ jwtSecret: config.get('JWT_SECRET'),
435
+ realmExtractorInstance: realmExtractor,
436
+ }),
437
+ })
438
+ ```
913
439
 
914
- async onPasswordReset(user: IAuthUser): Promise<void> {
915
- // Log security event, notify user, etc.
916
- console.log(`Password reset: ${user.email}`);
917
- }
440
+ `RealmMiddleware` is automatically registered by `AuthModule` — no manual middleware registration needed. When a `realmExtractorInstance` is provided, the middleware calls `extractor.extractRealm(request)` on every request before any guards run. If the extractor returns `null`, the request is rejected with `400 Bad Request` (strict mode).
918
441
 
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
- }
442
+ **Use `@CurrentRealm()` in resolvers:**
923
443
 
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
- }
444
+ ```typescript
445
+ import { CurrentRealm } from '@ambushsoftworks/nestjs-auth-graphql';
928
446
 
929
- async onSocialAccountUnlinked(user: IAuthUser, provider: string): Promise<void> {
930
- // Send confirmation email, log event, etc.
931
- console.log(`${provider} account unlinked: ${user.email}`);
447
+ @Resolver()
448
+ export class AuthResolver extends BaseAuthResolver<User> {
449
+ @Mutation(() => AuthResponse)
450
+ async signup(
451
+ @Args('input') input: SignupInput,
452
+ @CurrentRealm() realm: string,
453
+ @Context() ctx: any,
454
+ ) {
455
+ return this.performSignup(input, ctx);
932
456
  }
933
457
  }
934
458
  ```
935
459
 
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
460
+ `BaseAuthResolver` automatically extracts the realm from the GraphQL context via `getRealmFromContext()` and passes it to all service methods. Repository identity-resolution methods (`findByEmail`, `findByPhoneNumber`, `findByOAuthProvider`, `create`) receive the realm as a parameter so your implementation can scope queries accordingly.
964
461
 
965
- ### Biometric Authentication
462
+ **JWT realm validation:** The JWT payload includes a `realm` claim. On subsequent requests, `JwtStrategy` validates that the realm in the token matches the realm resolved from the current request. A mismatch results in `401 Unauthorized`.
966
463
 
967
- - **Public key cryptography**: WebAuthn-compatible
968
- - **Device enrollment**: Multiple device support
969
- - **Challenge-response**: Prevents replay attacks
464
+ **Rate limiter scoping:** When realms are enabled, rate limiter keys are automatically prefixed with the realm to prevent cross-realm interference (e.g., a lockout in realm A does not affect realm B).
970
465
 
971
- ## Security Best Practices
466
+ **Realms vs. Multi-Tenancy:**
972
467
 
973
- ### 🔐 Secret Management
468
+ | Concern | Realms | Multi-Tenancy |
469
+ |---------|--------|---------------|
470
+ | Partitions | Identity (who the user _is_) | Authorization (what the user _can access_) |
471
+ | Scope | User signup, login, tokens, verification | Permissions, resources, team membership |
472
+ | Same email in multiple? | Yes -- fully independent users | Yes -- same user, different tenant contexts |
473
+ | Guard layer | Middleware (before guards) | Guard (`TenantGuard`, after auth) |
974
474
 
975
- **CRITICAL**: Never commit secrets to version control. Use environment variables and secret management systems.
475
+ Realms and multi-tenancy are orthogonal and can be used together. For example, a white-label SaaS platform might use realms to isolate brand identities and tenancy to manage organizations within each brand.
976
476
 
977
- **Required Secrets:**
477
+ **Note:** Deprecated OAuth controller methods are not realm-aware. Use the current `BaseAuthResolver` OAuth methods for realm-scoped OAuth flows.
978
478
 
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.
479
+ ### API Key Authentication
984
480
 
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:**
481
+ For machine-to-machine auth, provide an `IApiKeyRepository`:
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
484
  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'),
485
+ useFactory: (apiKeyRepo) => ({
486
+ // ...required options
487
+ apiKeyRepositoryInstance: apiKeyRepo,
1029
488
  }),
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
- });
489
+ })
1046
490
  ```
1047
491
 
1048
- ### 🛡️ Token Configuration
1049
-
1050
- **Access Token Best Practices:**
492
+ Then use a multi-strategy guard:
1051
493
 
1052
494
  ```typescript
1053
- AuthModule.forRootAsync({
1054
- useFactory: (config) => ({
1055
- // Short-lived access tokens (minimize damage if compromised)
1056
- jwtExpiresIn: '15m', // Default: 15 minutes
495
+ { provide: APP_GUARD, useClass: createAuthGuard(['api-key', 'jwt'], { allowPublic: true }) }
496
+ ```
1057
497
 
1058
- // For mobile apps with poor connectivity, consider 1h max
1059
- // jwtExpiresIn: '1h', // Longer for mobile, but less secure
498
+ 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.
1060
499
 
1061
- // NEVER use long-lived access tokens
1062
- // jwtExpiresIn: '30d', // ❌ INSECURE
1063
- }),
1064
- });
1065
- ```
500
+ ### Email System
1066
501
 
1067
- **Refresh Token Best Practices:**
502
+ The email system uses a composable architecture: a **sender** (transport) and a **template renderer** (HTML generation).
1068
503
 
1069
504
  ```typescript
505
+ import { SendGridEmailSender } from '@ambushsoftworks/nestjs-auth-graphql';
506
+
1070
507
  AuthModule.forRootAsync({
1071
508
  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
509
+ // ...required options
510
+ email: {
511
+ sender: new SendGridEmailSender(config.get('SENDGRID_API_KEY')),
512
+ from: { email: 'noreply@example.com', name: 'MyApp' },
513
+ branding: {
514
+ appName: 'MyApp',
515
+ primaryColor: '#1976D2',
516
+ logoUrl: 'https://example.com/logo.png',
517
+ companyName: 'My Company',
518
+ supportEmail: 'support@example.com',
519
+ },
520
+ },
1080
521
  }),
1081
- });
522
+ })
1082
523
  ```
1083
524
 
1084
- **Token Rotation**: This package automatically rotates refresh tokens on each use. Old tokens are invalidated to prevent replay attacks.
525
+ `email.branding` is required when using the default template renderer. If you provide a custom `email.templateRenderer`, branding can be omitted.
1085
526
 
1086
- ### 🔒 Password Policy
527
+ **Built-in senders:** `SendGridEmailSender`, `ResendEmailSender`, `NoOpEmailSender`
1087
528
 
1088
- **Enforce Strong Passwords:**
529
+ **Custom sender:** Implement `IEmailSender`:
1089
530
 
1090
- The package includes default password validation (8+ characters, uppercase, lowercase, number, special character). For stronger policies:
531
+ ```typescript
532
+ class SmtpEmailSender implements IEmailSender {
533
+ async send(params: {
534
+ to: string;
535
+ from: { email: string; name?: string };
536
+ subject: string;
537
+ html: string;
538
+ text?: string;
539
+ }) {
540
+ // your SMTP logic
541
+ }
542
+ }
543
+ ```
544
+
545
+ **Custom template renderer:** Override `DefaultEmailTemplateRenderer` by providing `email.templateRenderer`:
1091
546
 
1092
547
  ```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;
548
+ email: {
549
+ sender: new SendGridEmailSender(apiKey),
550
+ from: { email: 'noreply@example.com' },
551
+ templateRenderer: new MyCustomRenderer(),
1111
552
  }
1112
553
  ```
1113
554
 
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)
555
+ ### Verification Modes
1119
556
 
1120
- ### ⏱️ Rate Limiting
557
+ Two modes for email verification and password reset:
1121
558
 
1122
- **Configure Throttling:**
559
+ - **`'code'`** (default) -- 6-digit numeric codes
560
+ - **`'token'`** -- URL-based tokens with configurable base URL
1123
561
 
1124
562
  ```typescript
1125
- import { ThrottlerModule } from '@nestjs/throttler';
563
+ features: { verificationMode: 'token' },
564
+ verification: {
565
+ baseUrl: 'https://app.example.com',
566
+ tokenLength: 64, // bytes, default: 64
567
+ tokenExpiresInMinutes: 60, // default: 60
568
+ },
569
+ ```
1126
570
 
1127
- @Module({
1128
- imports: [
1129
- // Global rate limiting
1130
- ThrottlerModule.forRoot([{
1131
- ttl: 60000, // 60 seconds
1132
- limit: 100, // 100 requests per minute
1133
- }]),
571
+ ### OAuth
1134
572
 
1135
- AuthModule.forRootAsync({
1136
- // Auth-specific throttling is handled per-mutation
1137
- // See AuthResolver for @Throttle() decorators
1138
- }),
1139
- ],
573
+ Google and Facebook OAuth with encrypted token storage (AES-256-GCM).
574
+
575
+ ```typescript
576
+ AuthModule.forRootAsync({
577
+ useFactory: (config) => ({
578
+ // ...required options
579
+ encryptionKey: config.get('ENCRYPTION_KEY'), // 32-byte hex string
580
+ oauth: {
581
+ google: {
582
+ clientId: config.get('GOOGLE_CLIENT_ID'),
583
+ clientSecret: config.get('GOOGLE_CLIENT_SECRET'),
584
+ callbackUrl: config.get('GOOGLE_CALLBACK_URL'),
585
+ },
586
+ facebook: {
587
+ clientId: config.get('FACEBOOK_CLIENT_ID'),
588
+ clientSecret: config.get('FACEBOOK_CLIENT_SECRET'),
589
+ callbackUrl: config.get('FACEBOOK_CALLBACK_URL'),
590
+ },
591
+ },
592
+ }),
1140
593
  })
1141
594
  ```
1142
595
 
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)
596
+ The `OAuthController` is automatically registered and handles OAuth callback routes. OAuth strategies gracefully degrade to no-ops when credentials are not configured.
1148
597
 
1149
- **Brute Force Protection**: Enabled by default - 5 failed login attempts = 15-minute account lockout.
598
+ ### Brute Force Protection
1150
599
 
1151
- ### 🌐 HTTPS/TLS Requirements
600
+ 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.
1152
601
 
1153
- **PRODUCTION REQUIREMENT**: All authentication endpoints MUST use HTTPS.
602
+ ### Password Policy
1154
603
 
1155
604
  ```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);
605
+ passwordPolicy: {
606
+ minLength: 12, // default: 8
607
+ maxLength: 72, // bcrypt limit
608
+ requireUppercase: true, // default: true
609
+ requireLowercase: true, // default: true
610
+ requireNumber: true, // default: true
611
+ requireSpecialChar: true, // default: false
612
+ customValidator: async (password) => {
613
+ // Optional: dictionary checks, leaked password detection, etc.
614
+ const isCommon = await checkLeakedPasswords(password);
615
+ return {
616
+ isValid: !isCommon,
617
+ errors: isCommon ? ['Password found in data breaches'] : [],
618
+ };
619
+ },
1174
620
  }
1175
- bootstrap();
1176
621
  ```
1177
622
 
1178
- **OAuth Callback URLs**: Must be HTTPS in production (required by Google/Facebook).
1179
-
1180
- ### 📊 Security Logging
623
+ ### Lifecycle Hooks
1181
624
 
1182
- **Implement Custom Security Logger** for production monitoring:
625
+ React to auth events without modifying core logic:
1183
626
 
1184
627
  ```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
- }
628
+ lifecycleHooksInstance: {
629
+ async onSignup(user) { /* create default settings, send analytics */ },
630
+ async onLogin(user) { /* update metrics */ },
631
+ async onLogout(user) { /* cleanup */ },
632
+ async onEmailVerified(user) { /* unlock features */ },
633
+ async onPhoneVerified(user) { /* enable 2FA */ },
634
+ async onOAuthAccountLinked(user, provider) { /* sync data */ },
635
+ async onPasswordReset(user) { /* send security alert */ },
636
+ async onPasswordChanged(user) { /* notify user */ },
637
+ async onAccountDelete(user) { /* cleanup user data */ },
638
+ onAuthFailure(email, ip, reason) { /* security monitoring */ },
1231
639
  }
1232
640
  ```
1233
641
 
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
642
+ 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).
1239
643
 
1240
- ### 🚀 Production Deployment
644
+ ### JWT Validation Modes
1241
645
 
1242
- **Environment Separation:**
646
+ - **`'full'`** (default) -- Calls `userRepository.findById()` on every request to verify the user still exists.
647
+ - **`'payload-only'`** -- Returns `{ id, email }` from the JWT payload without a DB lookup. Faster, but does not detect deleted/disabled users until token expiry.
1243
648
 
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
649
+ ```typescript
650
+ jwtValidation: 'payload-only',
1259
651
  ```
1260
652
 
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:**
653
+ ### JWT Payload Factory
1280
654
 
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
- }));
655
+ Customize JWT claims by providing an `IJwtPayloadFactory`:
1302
656
 
1303
- await app.listen(3000);
657
+ ```typescript
658
+ jwtPayloadFactoryInstance: {
659
+ createPayload(user) {
660
+ return { sub: user.id, email: user.email, role: user.role };
661
+ },
662
+ extractUserId(payload) {
663
+ return payload.sub;
664
+ },
1304
665
  }
1305
666
  ```
1306
667
 
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
- ```
668
+ ---
1352
669
 
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
- ```
670
+ ## Configuration Reference
1369
671
 
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
- ```
672
+ ### Required Options
1385
673
 
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
- ```
674
+ | Option | Type | Description |
675
+ |--------|------|-------------|
676
+ | `userRepositoryInstance` | `IUserRepository` | User persistence |
677
+ | `refreshTokenRepositoryInstance` | `IRefreshTokenRepository` | Token persistence |
678
+ | `jwtSecret` | `string` | JWT signing secret |
679
+
680
+ ### Common Options
681
+
682
+ | Option | Type | Default | Description |
683
+ |--------|------|---------|-------------|
684
+ | `jwtExpiresIn` | `string` | `'15m'` | Access token TTL |
685
+ | `refreshTokenExpiresIn` | `string` | `'30d'` | Refresh token TTL |
686
+ | `jwtValidation` | `'full' \| 'payload-only'` | `'full'` | JWT validation strategy |
687
+ | `bcryptRounds` | `number` | `12` | Password hashing cost |
688
+ | `nodeEnv` | `string` | `'production'` | Environment identifier |
689
+ | `passwordPolicy` | `PasswordPolicyConfig` | See [Password Policy](#password-policy) | Password strength rules |
690
+ | `encryptionKey` | `string` | -- | 32-byte hex string for AES-256-GCM (OAuth token encryption) |
691
+
692
+ ### Feature Flags (`features`)
693
+
694
+ | Flag | Default | Description |
695
+ |------|---------|-------------|
696
+ | `cookieAuth` | `false` | Token delivery via HttpOnly cookies |
697
+ | `multiTenancy` | `false` | Enable TenantGuard + PermissionGuard |
698
+ | `bruteForceProtection` | `false` | Account lockout on failed logins |
699
+ | `preventEnumerationOnSignup` | `false` | Return synthetic success for duplicate emails |
700
+ | `verificationMode` | `'code'` | `'code'` for 6-digit codes, `'token'` for URL tokens |
701
+ | `emailVerification` | `false` | Enable email verification flow |
702
+ | `smsVerification` | `false` | Enable SMS verification flow |
703
+ | `googleOAuth` | `false` | Enable Google OAuth |
704
+ | `facebookOAuth` | `false` | Enable Facebook OAuth |
705
+ | `biometricAuth` | `false` | Enable biometric authentication |
706
+
707
+ ### Security Secrets
708
+
709
+ Separate HMAC secrets for security isolation. All fall back to `jwtSecret` if not provided.
710
+
711
+ | Option | Type | Description |
712
+ |--------|------|-------------|
713
+ | `refreshTokenSecret` | `string` | HMAC secret for refresh token hashing |
714
+ | `verificationCodeSecret` | `string` | HMAC secret for verification code hashing |
715
+ | `oauthStateSecret` | `string` | JWT secret for OAuth CSRF state tokens |
716
+ | `oauthRedirectWhitelist` | `string` | Comma-separated allowed OAuth redirect URLs |
717
+
718
+ ### Cookie Options (`cookie`)
719
+
720
+ | Option | Type | Default | Description |
721
+ |--------|------|---------|-------------|
722
+ | `httpOnly` | `boolean` | `true` | HttpOnly flag |
723
+ | `secure` | `boolean` | `true` | Secure flag |
724
+ | `sameSite` | `'lax' \| 'strict' \| 'none'` | `'lax'` | SameSite attribute |
725
+ | `domain` | `string` | `undefined` | Cookie domain |
726
+ | `path` | `string` | `'/'` | Cookie path |
727
+ | `accessTokenName` | `string` | `'access_token'` | Access token cookie name |
728
+ | `refreshTokenName` | `string` | `'refresh_token'` | Refresh token cookie name |
729
+ | `accessTokenMaxAge` | `number` | derived from `jwtExpiresIn` | Access token cookie max age (ms) |
730
+ | `refreshTokenMaxAge` | `number` | derived from `refreshTokenExpiresIn` | Refresh token cookie max age (ms) |
731
+ | `useHostPrefix` | `boolean` | `false` | Prefix names with `__Host-` |
732
+
733
+ ### CSRF Options (`csrf`)
734
+
735
+ | Option | Type | Default | Description |
736
+ |--------|------|---------|-------------|
737
+ | `headerName` | `string` | `'X-Requested-With'` | Header to check |
738
+ | `requireInProduction` | `boolean` | `true` | Require CSRF header in production |
739
+ | `exemptOperations` | `string[]` | `[]` | GraphQL operations to skip |
740
+
741
+ ### Composable Email (`email`)
742
+
743
+ | Option | Type | Description |
744
+ |--------|------|-------------|
745
+ | `email.sender` | `IEmailSender` | Transport (SendGrid, Resend, etc.) -- required |
746
+ | `email.from` | `{ email, name? }` | Sender address -- required |
747
+ | `email.branding` | `EmailBrandingConfig` | App name, colors, logo, etc. -- required unless `templateRenderer` is provided |
748
+ | `email.templateRenderer` | `IEmailTemplateRenderer` | Override default HTML renderer |
749
+
750
+ When `email` is provided, it takes precedence over `emailServiceInstance`.
751
+
752
+ ### Verification Options (`verification`)
753
+
754
+ | Option | Type | Default | Description |
755
+ |--------|------|---------|-------------|
756
+ | `tokenLength` | `number` | `64` | Token length in bytes (token mode) |
757
+ | `tokenExpiresInMinutes` | `number` | `60` | Token expiration (token mode) |
758
+ | `baseUrl` | `string` | -- | Base URL for verification/reset URLs |
759
+
760
+ ### Optional Instance Options
761
+
762
+ | Option | Interface | Fallback |
763
+ |--------|-----------|----------|
764
+ | `emailServiceInstance` | `IEmailService` | `NoOpEmailService` |
765
+ | `smsServiceInstance` | `ISmsService` | `NoOpSmsService` |
766
+ | `lifecycleHooksInstance` | `IAuthLifecycleHooks` | `{}` (no-op) |
767
+ | `verificationRepositoryInstance` | `IVerificationRepository` | `NoOpVerificationRepository` |
768
+ | `bruteForceRepositoryInstance` | `IBruteForceRepository` | `NoOpBruteForceRepository` |
769
+ | `biometricRepositoryInstance` | `IBiometricRepository` | `NoOpBiometricRepository` |
770
+ | `authLoggerInstance` | `IAuthLogger` | `ConsoleAuthLogger` |
771
+ | `tenantRepositoryInstance` | `ITenantRepository` | `null` |
772
+ | `tenantExtractorInstance` | `ITenantExtractor` | `null` |
773
+ | `resourcePermissionRepositoryInstance` | `IResourcePermissionRepository` | `null` |
774
+ | `jwtPayloadFactoryInstance` | `IJwtPayloadFactory` | `DefaultJwtPayloadFactory` |
775
+ | `apiKeyRepositoryInstance` | `IApiKeyRepository` | `null` |
776
+ | `rateLimiterInstance` | `IRateLimiter` | `InMemoryRateLimiterService` |
777
+ | `realmExtractorInstance` | `IRealmExtractor` | `null` (realms disabled) |
1401
778
 
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
- ```
779
+ ---
1414
780
 
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
- ```
781
+ ## Decorators
1430
782
 
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
- );
1444
- ```
783
+ | Decorator | Target | Description |
784
+ |-----------|--------|-------------|
785
+ | `@Public()` | Method/Class | Skip authentication |
786
+ | `@PublicEndpoint()` | Method/Class | Skip authentication + tenant resolution (combines `@Public()` + `@SkipTenant()`) |
787
+ | `@SkipTenant()` | Method/Class | Skip tenant resolution |
788
+ | `@RequirePermissions('p1', 'p2')` | Method/Class | Require specific permissions |
789
+ | `@CurrentUser()` | Parameter | Inject authenticated user |
790
+ | `@CurrentTenant()` | Parameter | Inject resolved tenant context |
791
+ | `@CurrentRealm()` | Parameter | Inject resolved realm from request context |
792
+ | `@ResourceScope('resourceType', 'argName')` | Method | Resource-level permission scoping |
1445
793
 
1446
- ### Option 2: Adapter Pattern (Flexible Schema)
794
+ ## Guards
1447
795
 
1448
- If your database schema differs from the package expectations, create **adapter repositories** that translate between your schema and the package interfaces.
796
+ | Guard | Description |
797
+ |-------|-------------|
798
+ | `createAuthGuard(strategies, options)` | Factory for multi-strategy auth guards |
799
+ | `CsrfGuard` | CSRF header validation for cookie auth |
800
+ | `TenantGuard` | Multi-tenant context resolution |
801
+ | `PermissionGuard` | Permission checking against tenant context |
802
+ | `JwtAuthGuard` | Basic JWT guard (prefer `createAuthGuard`) |
1449
803
 
1450
- **Example**: Lift app uses a unified `VerificationCode` table for both email and SMS verification:
804
+ ### Recommended Guard Chains
1451
805
 
1452
- ```typescript
1453
- @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
- });
1482
- }
1483
-
1484
- // Implement other methods with similar adapter logic...
1485
- }
1486
- ```
806
+ When realm support is enabled, `RealmMiddleware` runs before all guards (as NestJS middleware) and attaches the resolved realm to `request._realm`. It is registered automatically by `AuthModule` -- see [Realm-Based Identity Isolation](#realm-based-identity-isolation).
1487
807
 
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.
808
+ ```typescript
809
+ // JWT only
810
+ { provide: APP_GUARD, useClass: createAuthGuard(['jwt'], { allowPublic: true }) }
1494
811
 
1495
- ## Environment Variables
812
+ // JWT + API keys
813
+ { provide: APP_GUARD, useClass: createAuthGuard(['jwt', 'api-key'], { allowPublic: true }) }
1496
814
 
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
815
+ // Full stack with cookie auth + multi-tenancy
816
+ { provide: APP_GUARD, useClass: CsrfGuard },
817
+ { provide: APP_GUARD, useClass: createAuthGuard(['jwt', 'api-key'], { allowPublic: true }) },
818
+ { provide: APP_GUARD, useClass: TenantGuard },
819
+ { provide: APP_GUARD, useClass: PermissionGuard },
1523
820
  ```
1524
821
 
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
- ```
822
+ ## Utilities
1534
823
 
1535
- ## Architecture
824
+ Helper functions exported for use in custom guards and middleware:
1536
825
 
1537
- This package follows **Layer 0 architecture** with complete interface abstraction:
826
+ | Function | Description |
827
+ |----------|-------------|
828
+ | `getRequestFromContext(context)` | Extract request from `ExecutionContext` (handles GraphQL + HTTP) |
829
+ | `extractIpAddress(request)` | Extract client IP (checks `clientIp`, `req.ip`, falls back to `'unknown'`) |
1538
830
 
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
831
+ ```typescript
832
+ import { getRequestFromContext, extractIpAddress } from '@ambushsoftworks/nestjs-auth-graphql';
1544
833
 
1545
- ## Migration from Lift Codebase
834
+ @Injectable()
835
+ export class CustomGuard implements CanActivate {
836
+ canActivate(context: ExecutionContext): boolean {
837
+ const request = getRequestFromContext(context);
838
+ const ip = extractIpAddress(request);
839
+ // your logic
840
+ return true;
841
+ }
842
+ }
843
+ ```
1546
844
 
1547
- If you're migrating from the Lift codebase:
845
+ ## Security Features
1548
846
 
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)
847
+ - **Refresh token rotation** with HMAC-SHA256 hashing and idempotent 10-second grace period
848
+ - **Bcrypt password hashing** with configurable rounds (default: 12)
849
+ - **AES-256-GCM encryption** for OAuth tokens at rest
850
+ - **Constant-time comparison** for verification codes
851
+ - **Account enumeration prevention** on signup (optional)
852
+ - **Stateless CSRF protection** for OAuth flows via `OAuthStateService`
853
+ - **`__Host-` cookie prefix** support for enhanced cookie security
854
+ - **Separate HMAC secrets** for refresh tokens, verification codes, and OAuth state
855
+ - **Realm-based identity isolation** with JWT realm claim validation and realm-scoped rate limiting
1553
856
 
1554
857
  ## License
1555
858
 
1556
- MIT
1557
-
1558
- ## Support
1559
-
1560
- For issues and questions, please open an issue on GitLab.
859
+ MIT -- see [LICENSE](./LICENSE).