@ambushsoftworks/nestjs-auth-graphql 0.2.0 → 0.3.0

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 (223) hide show
  1. package/README.md +759 -0
  2. package/dist/auth.module.d.ts +4 -1
  3. package/dist/auth.module.d.ts.map +1 -1
  4. package/dist/auth.module.js +20 -1
  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-user.decorator.js +1 -1
  11. package/dist/decorators/current-user.decorator.js.map +1 -1
  12. package/dist/dto/biometric-challenge.dto.d.ts.map +1 -1
  13. package/dist/dto/biometric-challenge.dto.js.map +1 -1
  14. package/dist/dto/biometric-credential.dto.d.ts.map +1 -1
  15. package/dist/dto/biometric-credential.dto.js.map +1 -1
  16. package/dist/dto/biometric-login.input.d.ts.map +1 -1
  17. package/dist/dto/biometric-login.input.js.map +1 -1
  18. package/dist/dto/biometric-status.dto.d.ts.map +1 -1
  19. package/dist/dto/biometric-status.dto.js.map +1 -1
  20. package/dist/dto/complete-facebook-signup.input.d.ts +6 -1
  21. package/dist/dto/complete-facebook-signup.input.d.ts.map +1 -1
  22. package/dist/dto/complete-facebook-signup.input.js.map +1 -1
  23. package/dist/dto/enable-biometric.input.d.ts.map +1 -1
  24. package/dist/dto/enable-biometric.input.js.map +1 -1
  25. package/dist/dto/enroll-biometric.input.d.ts.map +1 -1
  26. package/dist/dto/enroll-biometric.input.js.map +1 -1
  27. package/dist/dto/link-google-account.input.d.ts.map +1 -1
  28. package/dist/dto/link-google-account.input.js.map +1 -1
  29. package/dist/dto/login.input.d.ts.map +1 -1
  30. package/dist/dto/login.input.js +27 -2
  31. package/dist/dto/login.input.js.map +1 -1
  32. package/dist/dto/logout.input.d.ts.map +1 -1
  33. package/dist/dto/logout.input.js +3 -0
  34. package/dist/dto/logout.input.js.map +1 -1
  35. package/dist/dto/password-reset-response.dto.d.ts +11 -0
  36. package/dist/dto/password-reset-response.dto.d.ts.map +1 -0
  37. package/dist/dto/password-reset-response.dto.js +7 -0
  38. package/dist/dto/password-reset-response.dto.js.map +1 -0
  39. package/dist/dto/refresh-token.input.d.ts.map +1 -1
  40. package/dist/dto/refresh-token.input.js +22 -2
  41. package/dist/dto/refresh-token.input.js.map +1 -1
  42. package/dist/dto/remove-biometric-device-response.dto.d.ts.map +1 -1
  43. package/dist/dto/remove-biometric-device-response.dto.js.map +1 -1
  44. package/dist/dto/request-password-reset.input.d.ts +7 -0
  45. package/dist/dto/request-password-reset.input.d.ts.map +1 -0
  46. package/dist/dto/request-password-reset.input.js +21 -0
  47. package/dist/dto/request-password-reset.input.js.map +1 -0
  48. package/dist/dto/reset-password.input.d.ts +11 -0
  49. package/dist/dto/reset-password.input.d.ts.map +1 -0
  50. package/dist/dto/reset-password.input.js +31 -0
  51. package/dist/dto/reset-password.input.js.map +1 -0
  52. package/dist/dto/send-phone-verification.input.d.ts.map +1 -1
  53. package/dist/dto/send-phone-verification.input.js +10 -0
  54. package/dist/dto/send-phone-verification.input.js.map +1 -1
  55. package/dist/dto/signup.input.d.ts.map +1 -1
  56. package/dist/dto/signup.input.js +31 -2
  57. package/dist/dto/signup.input.js.map +1 -1
  58. package/dist/dto/unlink-social-account-response.dto.d.ts.map +1 -1
  59. package/dist/dto/unlink-social-account-response.dto.js.map +1 -1
  60. package/dist/dto/unlink-social-account.input.d.ts.map +1 -1
  61. package/dist/dto/unlink-social-account.input.js.map +1 -1
  62. package/dist/dto/verify-biometric-signature.input.d.ts.map +1 -1
  63. package/dist/dto/verify-biometric-signature.input.js.map +1 -1
  64. package/dist/dto/verify-email.input.d.ts.map +1 -1
  65. package/dist/dto/verify-email.input.js +4 -0
  66. package/dist/dto/verify-email.input.js.map +1 -1
  67. package/dist/dto/verify-phone.input.d.ts.map +1 -1
  68. package/dist/dto/verify-phone.input.js +7 -0
  69. package/dist/dto/verify-phone.input.js.map +1 -1
  70. package/dist/entities/auth-user.entity.d.ts.map +1 -1
  71. package/dist/enums/verification-type.enum.d.ts +9 -0
  72. package/dist/enums/verification-type.enum.d.ts.map +1 -0
  73. package/dist/enums/verification-type.enum.js +11 -0
  74. package/dist/enums/verification-type.enum.js.map +1 -0
  75. package/dist/exceptions/password-reset.exceptions.d.ts +8 -0
  76. package/dist/exceptions/password-reset.exceptions.d.ts.map +1 -0
  77. package/dist/exceptions/password-reset.exceptions.js +25 -0
  78. package/dist/exceptions/password-reset.exceptions.js.map +1 -0
  79. package/dist/index.d.ts +15 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +15 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts +1 -1
  84. package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts.map +1 -1
  85. package/dist/interfaces/auth-user.interface.d.ts +1 -0
  86. package/dist/interfaces/auth-user.interface.d.ts.map +1 -1
  87. package/dist/interfaces/magic-link-repository.interface.d.ts +6 -0
  88. package/dist/interfaces/magic-link-repository.interface.d.ts.map +1 -0
  89. package/dist/interfaces/magic-link-repository.interface.js +3 -0
  90. package/dist/interfaces/magic-link-repository.interface.js.map +1 -0
  91. package/dist/interfaces/password-policy-config.interface.d.ts +16 -0
  92. package/dist/interfaces/password-policy-config.interface.d.ts.map +1 -0
  93. package/dist/interfaces/password-policy-config.interface.js +3 -0
  94. package/dist/interfaces/password-policy-config.interface.js.map +1 -0
  95. package/dist/interfaces/password-reset-strategy.interface.d.ts +7 -0
  96. package/dist/interfaces/password-reset-strategy.interface.d.ts.map +1 -0
  97. package/dist/interfaces/password-reset-strategy.interface.js +3 -0
  98. package/dist/interfaces/password-reset-strategy.interface.js.map +1 -0
  99. package/dist/interfaces/rate-limiter.interface.d.ts +9 -0
  100. package/dist/interfaces/rate-limiter.interface.d.ts.map +1 -0
  101. package/dist/interfaces/rate-limiter.interface.js +3 -0
  102. package/dist/interfaces/rate-limiter.interface.js.map +1 -0
  103. package/dist/interfaces/user-repository.interface.d.ts +14 -10
  104. package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
  105. package/dist/interfaces/verification-repository.interface.d.ts +5 -4
  106. package/dist/interfaces/verification-repository.interface.d.ts.map +1 -1
  107. package/dist/repositories/noop-biometric.repository.d.ts +7 -4
  108. package/dist/repositories/noop-biometric.repository.d.ts.map +1 -1
  109. package/dist/repositories/noop-biometric.repository.js +22 -8
  110. package/dist/repositories/noop-biometric.repository.js.map +1 -1
  111. package/dist/repositories/noop-brute-force.repository.d.ts +3 -0
  112. package/dist/repositories/noop-brute-force.repository.d.ts.map +1 -1
  113. package/dist/repositories/noop-brute-force.repository.js +16 -2
  114. package/dist/repositories/noop-brute-force.repository.js.map +1 -1
  115. package/dist/repositories/noop-magic-link.repository.d.ts +9 -0
  116. package/dist/repositories/noop-magic-link.repository.d.ts.map +1 -0
  117. package/dist/repositories/noop-magic-link.repository.js +37 -0
  118. package/dist/repositories/noop-magic-link.repository.js.map +1 -0
  119. package/dist/repositories/noop-rate-limiter.d.ts +12 -0
  120. package/dist/repositories/noop-rate-limiter.d.ts.map +1 -0
  121. package/dist/repositories/noop-rate-limiter.js +38 -0
  122. package/dist/repositories/noop-rate-limiter.js.map +1 -0
  123. package/dist/repositories/noop-verification.repository.d.ts +8 -4
  124. package/dist/repositories/noop-verification.repository.d.ts.map +1 -1
  125. package/dist/repositories/noop-verification.repository.js +18 -3
  126. package/dist/repositories/noop-verification.repository.js.map +1 -1
  127. package/dist/resolvers/base-auth.resolver.d.ts +7 -0
  128. package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
  129. package/dist/resolvers/base-auth.resolver.js +20 -0
  130. package/dist/resolvers/base-auth.resolver.js.map +1 -1
  131. package/dist/resolvers/oauth.controller.d.ts.map +1 -1
  132. package/dist/resolvers/oauth.controller.js +16 -6
  133. package/dist/resolvers/oauth.controller.js.map +1 -1
  134. package/dist/services/auth.service.d.ts +15 -2
  135. package/dist/services/auth.service.d.ts.map +1 -1
  136. package/dist/services/auth.service.js +155 -17
  137. package/dist/services/auth.service.js.map +1 -1
  138. package/dist/services/biometric-auth.service.d.ts +4 -2
  139. package/dist/services/biometric-auth.service.d.ts.map +1 -1
  140. package/dist/services/biometric-auth.service.js +16 -6
  141. package/dist/services/biometric-auth.service.js.map +1 -1
  142. package/dist/services/biometric-verification.service.d.ts.map +1 -1
  143. package/dist/services/biometric-verification.service.js +9 -7
  144. package/dist/services/biometric-verification.service.js.map +1 -1
  145. package/dist/services/brute-force-protection.service.d.ts +0 -5
  146. package/dist/services/brute-force-protection.service.d.ts.map +1 -1
  147. package/dist/services/brute-force-protection.service.js +0 -5
  148. package/dist/services/brute-force-protection.service.js.map +1 -1
  149. package/dist/services/in-memory-rate-limiter.service.d.ts +15 -0
  150. package/dist/services/in-memory-rate-limiter.service.d.ts.map +1 -0
  151. package/dist/services/in-memory-rate-limiter.service.js +75 -0
  152. package/dist/services/in-memory-rate-limiter.service.js.map +1 -0
  153. package/dist/services/oauth-linking-token.service.d.ts.map +1 -1
  154. package/dist/services/oauth-linking-token.service.js +8 -2
  155. package/dist/services/oauth-linking-token.service.js.map +1 -1
  156. package/dist/services/oauth-state.service.d.ts.map +1 -1
  157. package/dist/services/oauth-state.service.js +3 -2
  158. package/dist/services/oauth-state.service.js.map +1 -1
  159. package/dist/services/password-validation.service.d.ts +11 -0
  160. package/dist/services/password-validation.service.d.ts.map +1 -0
  161. package/dist/services/password-validation.service.js +75 -0
  162. package/dist/services/password-validation.service.js.map +1 -0
  163. package/dist/services/refresh-token.service.d.ts +4 -1
  164. package/dist/services/refresh-token.service.d.ts.map +1 -1
  165. package/dist/services/refresh-token.service.js +7 -1
  166. package/dist/services/refresh-token.service.js.map +1 -1
  167. package/dist/services/sendgrid-email.service.d.ts +1 -1
  168. package/dist/services/sendgrid-email.service.d.ts.map +1 -1
  169. package/dist/services/sendgrid-email.service.js +90 -107
  170. package/dist/services/sendgrid-email.service.js.map +1 -1
  171. package/dist/services/twilio-sms.service.d.ts.map +1 -1
  172. package/dist/services/twilio-sms.service.js +6 -3
  173. package/dist/services/twilio-sms.service.js.map +1 -1
  174. package/dist/services/verification.service.d.ts.map +1 -1
  175. package/dist/services/verification.service.js +44 -30
  176. package/dist/services/verification.service.js.map +1 -1
  177. package/dist/strategies/facebook.strategy.d.ts.map +1 -1
  178. package/dist/strategies/facebook.strategy.js +4 -2
  179. package/dist/strategies/facebook.strategy.js.map +1 -1
  180. package/dist/strategies/google.strategy.d.ts.map +1 -1
  181. package/dist/strategies/google.strategy.js +2 -1
  182. package/dist/strategies/google.strategy.js.map +1 -1
  183. package/dist/strategies/jwt.strategy.d.ts +0 -1
  184. package/dist/strategies/jwt.strategy.d.ts.map +1 -1
  185. package/dist/strategies/jwt.strategy.js +0 -1
  186. package/dist/strategies/jwt.strategy.js.map +1 -1
  187. package/dist/strategies/magic-link.strategy.d.ts +16 -0
  188. package/dist/strategies/magic-link.strategy.d.ts.map +1 -0
  189. package/dist/strategies/magic-link.strategy.js +80 -0
  190. package/dist/strategies/magic-link.strategy.js.map +1 -0
  191. package/dist/strategies/noop-facebook.strategy.d.ts +1 -1
  192. package/dist/strategies/noop-facebook.strategy.d.ts.map +1 -1
  193. package/dist/strategies/noop-facebook.strategy.js +1 -1
  194. package/dist/strategies/noop-facebook.strategy.js.map +1 -1
  195. package/dist/strategies/noop-google.strategy.d.ts +1 -1
  196. package/dist/strategies/noop-google.strategy.d.ts.map +1 -1
  197. package/dist/strategies/noop-google.strategy.js +1 -1
  198. package/dist/strategies/noop-google.strategy.js.map +1 -1
  199. package/dist/strategies/verification-code.strategy.d.ts +11 -0
  200. package/dist/strategies/verification-code.strategy.d.ts.map +1 -0
  201. package/dist/strategies/verification-code.strategy.js +44 -0
  202. package/dist/strategies/verification-code.strategy.js.map +1 -0
  203. package/dist/test-utils/index.d.ts +5 -0
  204. package/dist/test-utils/index.d.ts.map +1 -0
  205. package/dist/test-utils/index.js +40 -0
  206. package/dist/test-utils/index.js.map +1 -0
  207. package/dist/test-utils/mock-repositories.d.ts +7 -0
  208. package/dist/test-utils/mock-repositories.d.ts.map +1 -0
  209. package/dist/test-utils/mock-repositories.js +84 -0
  210. package/dist/test-utils/mock-repositories.js.map +1 -0
  211. package/dist/test-utils/mock-services.d.ts +6 -0
  212. package/dist/test-utils/mock-services.d.ts.map +1 -0
  213. package/dist/test-utils/mock-services.js +47 -0
  214. package/dist/test-utils/mock-services.js.map +1 -0
  215. package/dist/test-utils/test-factories.d.ts +51 -0
  216. package/dist/test-utils/test-factories.d.ts.map +1 -0
  217. package/dist/test-utils/test-factories.js +201 -0
  218. package/dist/test-utils/test-factories.js.map +1 -0
  219. package/dist/test-utils/test-helpers.d.ts +24 -0
  220. package/dist/test-utils/test-helpers.d.ts.map +1 -0
  221. package/dist/test-utils/test-helpers.js +137 -0
  222. package/dist/test-utils/test-helpers.js.map +1 -0
  223. package/package.json +1 -1
package/README.md CHANGED
@@ -25,6 +25,7 @@ Production-grade authentication package for NestJS with GraphQL, extracted from
25
25
  - **OAuth 2.0**: Google and Facebook social login
26
26
  - **Email Verification**: 6-digit PIN codes via SendGrid with rate limiting
27
27
  - **SMS Verification**: Phone number verification via Twilio
28
+ - **Password Reset**: 6-digit verification codes with email enumeration protection
28
29
  - **Biometric Authentication**: Face ID, Touch ID, fingerprint support
29
30
  - **Brute Force Protection**: Account lockout after failed login attempts
30
31
  - **Account Linking**: Link/unlink social accounts to existing accounts
@@ -253,6 +254,93 @@ export class AuthResolver extends BaseAuthResolver<User> {
253
254
  export class AppModule {}
254
255
  ```
255
256
 
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`:
262
+
263
+ ```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
+ );
282
+
283
+ await app.listen(3000);
284
+ }
285
+ bootstrap();
286
+ ```
287
+
288
+ **What gets validated:**
289
+
290
+ The package validates all input fields with appropriate decorators:
291
+
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
300
+
301
+ **Validation error responses:**
302
+
303
+ When validation fails, NestJS automatically returns a `400 Bad Request` with detailed error messages:
304
+
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
+ ```
316
+
317
+ **Custom validation for your DTOs:**
318
+
319
+ If you create your own input DTOs implementing the package interfaces (recommended for full type control), add `class-validator` decorators:
320
+
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
+ ```
341
+
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.
343
+
256
344
  ## Configuration Options
257
345
 
258
346
  ### Required Options
@@ -370,6 +458,308 @@ import { NoOpEmailService, NoOpSmsService } from '@yourorg/nestjs-auth-graphql';
370
458
  })
371
459
  ```
372
460
 
461
+ ## Password Reset
462
+
463
+ Secure password reset flow with 6-digit verification codes, rate limiting, and email enumeration protection.
464
+
465
+ ### Features
466
+
467
+ - **6-Digit Verification Codes** - SMS/email verification pattern (not magic links)
468
+ - **Email Enumeration Protection** - Generic success messages for all requests
469
+ - **Rate Limiting** - 60-second cooldown between requests per user
470
+ - **Password Strength Validation** - Configurable requirements (default: 8+ chars, uppercase, lowercase, number)
471
+ - **Token Revocation** - All refresh tokens invalidated on password change
472
+ - **Brute Force Protection** - Integration with account locking system
473
+ - **OAuth User Protection** - Users authenticated via social login cannot reset passwords
474
+ - **Security Logging** - Audit trail for all password reset activities
475
+
476
+ ### Consumer Setup
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
+ ```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
507
+ }
508
+ ```
509
+
510
+ #### Step 2: Configure Email Service
511
+
512
+ Ensure `SendGridEmailService` (or your custom email service) is configured:
513
+
514
+ ```typescript
515
+ 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
+ },
531
+ }),
532
+ }),
533
+ ```
534
+
535
+ **Environment Variable:**
536
+ ```bash
537
+ FRONTEND_URL=https://yourapp.com # Used in email branding
538
+ ```
539
+
540
+ #### Step 3: Create GraphQL DTOs
541
+
542
+ Create consumer-specific DTOs with GraphQL decorators:
543
+
544
+ **`request-password-reset.input.ts`:**
545
+ ```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
+ }
556
+ ```
557
+
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;
570
+
571
+ @Field(() => String, { description: 'New password (8+ chars, uppercase, lowercase, number)' })
572
+ newPassword: string;
573
+ }
574
+ ```
575
+
576
+ **`password-reset-response.dto.ts`:**
577
+ ```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>;
633
+ }
634
+ }
635
+ ```
636
+
637
+ #### Step 5: Deploy & Test
638
+
639
+ ```bash
640
+ # Run migration
641
+ npx prisma migrate deploy
642
+
643
+ # Start dev server
644
+ npm run start:dev
645
+
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 } }"}'
650
+ ```
651
+
652
+ ### GraphQL Schema
653
+
654
+ After setup, your schema will include:
655
+
656
+ ```graphql
657
+ type Mutation {
658
+ requestPasswordReset(input: RequestPasswordResetInput!): PasswordResetResponse!
659
+ resetPassword(input: ResetPasswordInput!): PasswordResetResponse!
660
+ }
661
+
662
+ input RequestPasswordResetInput {
663
+ email: String!
664
+ }
665
+
666
+ input ResetPasswordInput {
667
+ email: String!
668
+ code: String!
669
+ newPassword: String!
670
+ }
671
+
672
+ type PasswordResetResponse {
673
+ success: Boolean!
674
+ message: String!
675
+ retryAfterSeconds: Int
676
+ }
677
+ ```
678
+
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):**
691
+
692
+ ```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
+ }
716
+ ```
717
+
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:
747
+
748
+ ```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
+ }
761
+ ```
762
+
373
763
  ## GraphQL API
374
764
 
375
765
  The package provides a complete GraphQL API:
@@ -426,6 +816,23 @@ mutation VerifyPhone($input: VerifyPhoneInput!) {
426
816
  }
427
817
  }
428
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
+
429
836
  # Google OAuth Account Linking
430
837
  mutation LinkGoogleAccount($input: LinkGoogleAccountInput!) {
431
838
  linkGoogleAccount(linkGoogleAccountInput: $input) {
@@ -561,6 +968,358 @@ export class MyAuthHooks implements IAuthLifecycleHooks {
561
968
  - **Device enrollment**: Multiple device support
562
969
  - **Challenge-response**: Prevents replay attacks
563
970
 
971
+ ## Security Best Practices
972
+
973
+ ### 🔐 Secret Management
974
+
975
+ **CRITICAL**: Never commit secrets to version control. Use environment variables and secret management systems.
976
+
977
+ **Required Secrets:**
978
+
979
+ 1. **JWT_SECRET**:
980
+ - Generate: `openssl rand -base64 64`
981
+ - Minimum: 32 bytes (256 bits)
982
+ - Rotate: Every 90 days or on suspected compromise
983
+ - Store: Environment variables, AWS Secrets Manager, HashiCorp Vault, etc.
984
+
985
+ 2. **ENCRYPTION_KEY** (for OAuth):
986
+ - Generate: `openssl rand -hex 32` (produces 64-character hex string)
987
+ - Required length: Exactly 32 bytes (64 hex characters)
988
+ - Purpose: AES-256-GCM encryption for OAuth access tokens at rest
989
+ - Auto-generated if not provided, but **provide explicitly in production** for consistency across instances
990
+
991
+ 3. **OAuth Secrets**:
992
+ - Never expose in client-side code
993
+ - Use environment-specific secrets (dev/staging/prod)
994
+ - Rotate on suspected compromise
995
+
996
+ **Example `.env` file** (never commit this file):
997
+ ```bash
998
+ # Generate with: openssl rand -base64 64
999
+ JWT_SECRET=your_random_64_byte_base64_secret
1000
+
1001
+ # Generate with: openssl rand -hex 32
1002
+ ENCRYPTION_KEY=your_64_character_hex_string
1003
+
1004
+ # OAuth secrets from provider dashboards
1005
+ GOOGLE_CLIENT_SECRET=...
1006
+ FACEBOOK_CLIENT_SECRET=...
1007
+
1008
+ # Third-party API keys
1009
+ SENDGRID_API_KEY=...
1010
+ TWILIO_AUTH_TOKEN=...
1011
+ ```
1012
+
1013
+ **Production Secret Management:**
1014
+
1015
+ ```typescript
1016
+ // ❌ BAD: Hard-coded secrets
1017
+ AuthModule.forRootAsync({
1018
+ useFactory: () => ({
1019
+ jwtSecret: 'my-secret-key', // NEVER DO THIS
1020
+ }),
1021
+ });
1022
+
1023
+ // ✅ GOOD: Environment variables
1024
+ AuthModule.forRootAsync({
1025
+ inject: [ConfigService],
1026
+ useFactory: (config: ConfigService) => ({
1027
+ jwtSecret: config.get<string>('JWT_SECRET'), // Read from env
1028
+ encryptionKey: config.get<string>('ENCRYPTION_KEY'),
1029
+ }),
1030
+ });
1031
+
1032
+ // ✅ BETTER: Validation with config module
1033
+ import { ConfigModule } from '@nestjs/config';
1034
+ import * as Joi from 'joi';
1035
+
1036
+ ConfigModule.forRoot({
1037
+ validationSchema: Joi.object({
1038
+ JWT_SECRET: Joi.string().min(32).required(),
1039
+ ENCRYPTION_KEY: Joi.string().length(64).pattern(/^[0-9a-f]{64}$/).required(),
1040
+ GOOGLE_CLIENT_SECRET: Joi.string().when('GOOGLE_CLIENT_ID', {
1041
+ is: Joi.exist(),
1042
+ then: Joi.required(),
1043
+ }),
1044
+ }),
1045
+ });
1046
+ ```
1047
+
1048
+ ### 🛡️ Token Configuration
1049
+
1050
+ **Access Token Best Practices:**
1051
+
1052
+ ```typescript
1053
+ AuthModule.forRootAsync({
1054
+ useFactory: (config) => ({
1055
+ // Short-lived access tokens (minimize damage if compromised)
1056
+ jwtExpiresIn: '15m', // Default: 15 minutes
1057
+
1058
+ // For mobile apps with poor connectivity, consider 1h max
1059
+ // jwtExpiresIn: '1h', // Longer for mobile, but less secure
1060
+
1061
+ // NEVER use long-lived access tokens
1062
+ // jwtExpiresIn: '30d', // ❌ INSECURE
1063
+ }),
1064
+ });
1065
+ ```
1066
+
1067
+ **Refresh Token Best Practices:**
1068
+
1069
+ ```typescript
1070
+ AuthModule.forRootAsync({
1071
+ useFactory: (config) => ({
1072
+ // Balance between security and user experience
1073
+ refreshTokenExpiresIn: '30d', // Default: 30 days
1074
+
1075
+ // For high-security applications, use shorter expiration
1076
+ // refreshTokenExpiresIn: '7d', // Re-authenticate weekly
1077
+
1078
+ // For consumer apps, longer is acceptable
1079
+ // refreshTokenExpiresIn: '90d', // Re-authenticate quarterly
1080
+ }),
1081
+ });
1082
+ ```
1083
+
1084
+ **Token Rotation**: This package automatically rotates refresh tokens on each use. Old tokens are invalidated to prevent replay attacks.
1085
+
1086
+ ### 🔒 Password Policy
1087
+
1088
+ **Enforce Strong Passwords:**
1089
+
1090
+ The package includes default password validation (8+ characters, uppercase, lowercase, number, special character). For stronger policies:
1091
+
1092
+ ```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;
1111
+ }
1112
+ ```
1113
+
1114
+ **Additional Recommendations:**
1115
+ - ✅ Check against common password lists (e.g., `have-i-been-pwned` API)
1116
+ - ✅ Implement password history (prevent reuse of last 5 passwords)
1117
+ - ✅ Require password change on first login (for admin-created accounts)
1118
+ - ✅ Implement password expiration (60-90 days for high-security apps)
1119
+
1120
+ ### ⏱️ Rate Limiting
1121
+
1122
+ **Configure Throttling:**
1123
+
1124
+ ```typescript
1125
+ import { ThrottlerModule } from '@nestjs/throttler';
1126
+
1127
+ @Module({
1128
+ imports: [
1129
+ // Global rate limiting
1130
+ ThrottlerModule.forRoot([{
1131
+ ttl: 60000, // 60 seconds
1132
+ limit: 100, // 100 requests per minute
1133
+ }]),
1134
+
1135
+ AuthModule.forRootAsync({
1136
+ // Auth-specific throttling is handled per-mutation
1137
+ // See AuthResolver for @Throttle() decorators
1138
+ }),
1139
+ ],
1140
+ })
1141
+ ```
1142
+
1143
+ **Per-Endpoint Throttling** (already implemented in BaseAuthResolver):
1144
+ - Login: 5 requests/minute (prevent brute force)
1145
+ - Signup: 5 requests/minute (prevent spam)
1146
+ - Refresh: 10 requests/minute (allow frequent refreshes)
1147
+ - Verification codes: 3 requests/minute (prevent SMS/email bombing)
1148
+
1149
+ **Brute Force Protection**: Enabled by default - 5 failed login attempts = 15-minute account lockout.
1150
+
1151
+ ### 🌐 HTTPS/TLS Requirements
1152
+
1153
+ **PRODUCTION REQUIREMENT**: All authentication endpoints MUST use HTTPS.
1154
+
1155
+ ```typescript
1156
+ // Production enforcement example
1157
+ import { NestFactory } from '@nestjs/core';
1158
+ import { AppModule } from './app.module';
1159
+
1160
+ async function bootstrap() {
1161
+ const app = await NestFactory.create(AppModule);
1162
+
1163
+ // Enforce HTTPS in production
1164
+ if (process.env.NODE_ENV === 'production') {
1165
+ app.use((req, res, next) => {
1166
+ if (!req.secure && req.headers['x-forwarded-proto'] !== 'https') {
1167
+ return res.redirect(301, `https://${req.headers.host}${req.url}`);
1168
+ }
1169
+ next();
1170
+ });
1171
+ }
1172
+
1173
+ await app.listen(3000);
1174
+ }
1175
+ bootstrap();
1176
+ ```
1177
+
1178
+ **OAuth Callback URLs**: Must be HTTPS in production (required by Google/Facebook).
1179
+
1180
+ ### 📊 Security Logging
1181
+
1182
+ **Implement Custom Security Logger** for production monitoring:
1183
+
1184
+ ```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
+ }
1231
+ }
1232
+ ```
1233
+
1234
+ **Critical Events to Monitor:**
1235
+ - `TOKEN_REUSE_DETECTED`: Possible security breach
1236
+ - `ACCOUNT_LOCKED`: High failed login attempts
1237
+ - `LOGIN_FAILURE`: Pattern analysis for attacks
1238
+ - `VERIFICATION_CODE_FAILED`: Potential brute force on codes
1239
+
1240
+ ### 🚀 Production Deployment
1241
+
1242
+ **Environment Separation:**
1243
+
1244
+ ```bash
1245
+ # Development (.env.development)
1246
+ NODE_ENV=development
1247
+ JWT_SECRET=dev_secret_key
1248
+ ENCRYPTION_KEY=dev_encryption_key
1249
+
1250
+ # Staging (.env.staging)
1251
+ NODE_ENV=staging
1252
+ JWT_SECRET=staging_secret_key_different_from_dev
1253
+ ENCRYPTION_KEY=staging_encryption_key_different_from_dev
1254
+
1255
+ # Production (.env.production)
1256
+ NODE_ENV=production
1257
+ JWT_SECRET=production_secret_key_from_secrets_manager
1258
+ ENCRYPTION_KEY=production_encryption_key_from_secrets_manager
1259
+ ```
1260
+
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:**
1280
+
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
+ }));
1302
+
1303
+ await app.listen(3000);
1304
+ }
1305
+ ```
1306
+
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
+
564
1323
  ## Database Schema Requirements
565
1324
 
566
1325
  This package expects specific database tables to implement the repository interfaces. You have two options: