@nauth-toolkit/core 0.1.87 → 0.1.89

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 (174) hide show
  1. package/dist/dto/admin-get-mfa-status.dto.d.ts +20 -0
  2. package/dist/dto/admin-get-mfa-status.dto.d.ts.map +1 -0
  3. package/dist/dto/{change-password-request.dto.js → admin-get-mfa-status.dto.js} +22 -32
  4. package/dist/dto/admin-get-mfa-status.dto.js.map +1 -0
  5. package/dist/dto/admin-get-user-auth-history.dto.d.ts +62 -0
  6. package/dist/dto/admin-get-user-auth-history.dto.d.ts.map +1 -0
  7. package/dist/dto/admin-get-user-auth-history.dto.js +87 -0
  8. package/dist/dto/admin-get-user-auth-history.dto.js.map +1 -0
  9. package/dist/dto/admin-logout-all.dto.d.ts +48 -0
  10. package/dist/dto/admin-logout-all.dto.d.ts.map +1 -0
  11. package/dist/dto/admin-logout-all.dto.js +85 -0
  12. package/dist/dto/admin-logout-all.dto.js.map +1 -0
  13. package/dist/dto/admin-remove-devices.dto.d.ts +25 -0
  14. package/dist/dto/admin-remove-devices.dto.d.ts.map +1 -0
  15. package/dist/dto/admin-remove-devices.dto.js +50 -0
  16. package/dist/dto/admin-remove-devices.dto.js.map +1 -0
  17. package/dist/dto/admin-reset-password.dto.d.ts +15 -19
  18. package/dist/dto/admin-reset-password.dto.d.ts.map +1 -1
  19. package/dist/dto/admin-reset-password.dto.js +21 -41
  20. package/dist/dto/admin-reset-password.dto.js.map +1 -1
  21. package/dist/dto/admin-revoke-session.dto.d.ts +22 -0
  22. package/dist/dto/admin-revoke-session.dto.d.ts.map +1 -0
  23. package/dist/dto/admin-revoke-session.dto.js +48 -0
  24. package/dist/dto/admin-revoke-session.dto.js.map +1 -0
  25. package/dist/dto/admin-set-password.dto.d.ts +8 -10
  26. package/dist/dto/admin-set-password.dto.d.ts.map +1 -1
  27. package/dist/dto/admin-set-password.dto.js +11 -21
  28. package/dist/dto/admin-set-password.dto.js.map +1 -1
  29. package/dist/dto/admin-set-preferred-method.dto.d.ts +25 -0
  30. package/dist/dto/admin-set-preferred-method.dto.d.ts.map +1 -0
  31. package/dist/dto/admin-set-preferred-method.dto.js +50 -0
  32. package/dist/dto/admin-set-preferred-method.dto.js.map +1 -0
  33. package/dist/dto/admin-update-user-attributes.dto.d.ts +41 -0
  34. package/dist/dto/admin-update-user-attributes.dto.d.ts.map +1 -0
  35. package/dist/dto/{update-user-attributes-request.dto.js → admin-update-user-attributes.dto.js} +12 -17
  36. package/dist/dto/admin-update-user-attributes.dto.js.map +1 -0
  37. package/dist/dto/auth-challenge.dto.d.ts +2 -2
  38. package/dist/dto/auth-challenge.dto.d.ts.map +1 -1
  39. package/dist/dto/auth-challenge.dto.js +3 -3
  40. package/dist/dto/auth-challenge.dto.js.map +1 -1
  41. package/dist/dto/auth-response.dto.d.ts +1 -1
  42. package/dist/dto/auth-response.dto.d.ts.map +1 -1
  43. package/dist/dto/auth-response.dto.js +1 -1
  44. package/dist/dto/auth-response.dto.js.map +1 -1
  45. package/dist/dto/get-mfa-status.dto.d.ts +3 -32
  46. package/dist/dto/get-mfa-status.dto.d.ts.map +1 -1
  47. package/dist/dto/get-mfa-status.dto.js +4 -55
  48. package/dist/dto/get-mfa-status.dto.js.map +1 -1
  49. package/dist/dto/get-risk-assessment-history.dto.d.ts +3 -3
  50. package/dist/dto/get-risk-assessment-history.dto.d.ts.map +1 -1
  51. package/dist/dto/get-risk-assessment-history.dto.js +5 -5
  52. package/dist/dto/get-risk-assessment-history.dto.js.map +1 -1
  53. package/dist/dto/get-suspicious-activity.dto.d.ts +3 -3
  54. package/dist/dto/get-suspicious-activity.dto.d.ts.map +1 -1
  55. package/dist/dto/get-suspicious-activity.dto.js +5 -5
  56. package/dist/dto/get-suspicious-activity.dto.js.map +1 -1
  57. package/dist/dto/get-user-auth-history.dto.d.ts +4 -39
  58. package/dist/dto/get-user-auth-history.dto.d.ts.map +1 -1
  59. package/dist/dto/get-user-auth-history.dto.js +53 -51
  60. package/dist/dto/get-user-auth-history.dto.js.map +1 -1
  61. package/dist/dto/get-user-devices.dto.d.ts +5 -18
  62. package/dist/dto/get-user-devices.dto.d.ts.map +1 -1
  63. package/dist/dto/get-user-devices.dto.js +5 -39
  64. package/dist/dto/get-user-devices.dto.js.map +1 -1
  65. package/dist/dto/get-user-sessions-response.dto.d.ts +1 -1
  66. package/dist/dto/get-user-sessions-response.dto.js +1 -1
  67. package/dist/dto/get-user-sessions.dto.d.ts +1 -1
  68. package/dist/dto/get-user-sessions.dto.js +1 -1
  69. package/dist/dto/index.d.ts +9 -2
  70. package/dist/dto/index.d.ts.map +1 -1
  71. package/dist/dto/index.js +9 -2
  72. package/dist/dto/index.js.map +1 -1
  73. package/dist/dto/logout-all-response.dto.d.ts +1 -1
  74. package/dist/dto/logout-all-response.dto.js +1 -1
  75. package/dist/dto/logout-all.dto.d.ts +1 -18
  76. package/dist/dto/logout-all.dto.d.ts.map +1 -1
  77. package/dist/dto/logout-all.dto.js +1 -30
  78. package/dist/dto/logout-all.dto.js.map +1 -1
  79. package/dist/dto/logout-session.dto.d.ts +0 -5
  80. package/dist/dto/logout-session.dto.d.ts.map +1 -1
  81. package/dist/dto/logout-session.dto.js +0 -12
  82. package/dist/dto/logout-session.dto.js.map +1 -1
  83. package/dist/dto/logout.dto.d.ts +1 -18
  84. package/dist/dto/logout.dto.d.ts.map +1 -1
  85. package/dist/dto/logout.dto.js +1 -30
  86. package/dist/dto/logout.dto.js.map +1 -1
  87. package/dist/dto/remove-devices.dto.d.ts +4 -16
  88. package/dist/dto/remove-devices.dto.d.ts.map +1 -1
  89. package/dist/dto/remove-devices.dto.js +4 -26
  90. package/dist/dto/remove-devices.dto.js.map +1 -1
  91. package/dist/dto/set-mfa-exemption.dto.d.ts +8 -9
  92. package/dist/dto/set-mfa-exemption.dto.d.ts.map +1 -1
  93. package/dist/dto/set-mfa-exemption.dto.js +11 -13
  94. package/dist/dto/set-mfa-exemption.dto.js.map +1 -1
  95. package/dist/dto/set-must-change-password.dto.d.ts +3 -3
  96. package/dist/dto/set-must-change-password.dto.d.ts.map +1 -1
  97. package/dist/dto/set-must-change-password.dto.js +5 -5
  98. package/dist/dto/set-must-change-password.dto.js.map +1 -1
  99. package/dist/dto/set-preferred-method.dto.d.ts +4 -16
  100. package/dist/dto/set-preferred-method.dto.d.ts.map +1 -1
  101. package/dist/dto/set-preferred-method.dto.js +4 -26
  102. package/dist/dto/set-preferred-method.dto.js.map +1 -1
  103. package/dist/dto/setup-mfa.dto.d.ts +3 -18
  104. package/dist/dto/setup-mfa.dto.d.ts.map +1 -1
  105. package/dist/dto/setup-mfa.dto.js +3 -30
  106. package/dist/dto/setup-mfa.dto.js.map +1 -1
  107. package/dist/dto/social-auth.dto.d.ts +4 -34
  108. package/dist/dto/social-auth.dto.d.ts.map +1 -1
  109. package/dist/dto/social-auth.dto.js +10 -68
  110. package/dist/dto/social-auth.dto.js.map +1 -1
  111. package/dist/dto/update-user-attributes.dto.d.ts +26 -0
  112. package/dist/dto/update-user-attributes.dto.d.ts.map +1 -0
  113. package/dist/dto/update-user-attributes.dto.js +30 -0
  114. package/dist/dto/update-user-attributes.dto.js.map +1 -0
  115. package/dist/index.d.ts +5 -0
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +5 -0
  118. package/dist/index.js.map +1 -1
  119. package/dist/interfaces/hooks.interface.d.ts +2 -1
  120. package/dist/interfaces/hooks.interface.d.ts.map +1 -1
  121. package/dist/interfaces/mfa-provider.interface.d.ts +7 -8
  122. package/dist/interfaces/mfa-provider.interface.d.ts.map +1 -1
  123. package/dist/interfaces/provider.interface.d.ts +1 -1
  124. package/dist/interfaces/provider.interface.d.ts.map +1 -1
  125. package/dist/services/adaptive-mfa-decision.service.js +2 -2
  126. package/dist/services/adaptive-mfa-decision.service.js.map +1 -1
  127. package/dist/services/admin-auth.service.d.ts +307 -0
  128. package/dist/services/admin-auth.service.d.ts.map +1 -0
  129. package/dist/services/admin-auth.service.js +885 -0
  130. package/dist/services/admin-auth.service.js.map +1 -0
  131. package/dist/services/auth-audit.service.d.ts +16 -16
  132. package/dist/services/auth-audit.service.d.ts.map +1 -1
  133. package/dist/services/auth-audit.service.js +33 -33
  134. package/dist/services/auth-audit.service.js.map +1 -1
  135. package/dist/services/auth-challenge-helper.service.js +3 -3
  136. package/dist/services/auth-challenge-helper.service.js.map +1 -1
  137. package/dist/services/auth-service-internal-helpers.d.ts +13 -2
  138. package/dist/services/auth-service-internal-helpers.d.ts.map +1 -1
  139. package/dist/services/auth-service-internal-helpers.js +39 -1
  140. package/dist/services/auth-service-internal-helpers.js.map +1 -1
  141. package/dist/services/auth.service.d.ts +94 -438
  142. package/dist/services/auth.service.d.ts.map +1 -1
  143. package/dist/services/auth.service.js +388 -1255
  144. package/dist/services/auth.service.js.map +1 -1
  145. package/dist/services/mfa-base.service.d.ts +14 -4
  146. package/dist/services/mfa-base.service.d.ts.map +1 -1
  147. package/dist/services/mfa-base.service.js +22 -1
  148. package/dist/services/mfa-base.service.js.map +1 -1
  149. package/dist/services/mfa.service.d.ts +107 -33
  150. package/dist/services/mfa.service.d.ts.map +1 -1
  151. package/dist/services/mfa.service.js +456 -333
  152. package/dist/services/mfa.service.js.map +1 -1
  153. package/dist/services/social-auth.service.d.ts +7 -0
  154. package/dist/services/social-auth.service.d.ts.map +1 -1
  155. package/dist/services/social-auth.service.js +38 -26
  156. package/dist/services/social-auth.service.js.map +1 -1
  157. package/dist/services/user.service.d.ts +3 -3
  158. package/dist/services/user.service.d.ts.map +1 -1
  159. package/dist/services/user.service.js +7 -7
  160. package/dist/services/user.service.js.map +1 -1
  161. package/dist/utils/dto-validator.d.ts.map +1 -1
  162. package/dist/utils/dto-validator.js +50 -4
  163. package/dist/utils/dto-validator.js.map +1 -1
  164. package/dist/utils/setup/init-services.d.ts +2 -1
  165. package/dist/utils/setup/init-services.d.ts.map +1 -1
  166. package/dist/utils/setup/init-services.js +2 -0
  167. package/dist/utils/setup/init-services.js.map +1 -1
  168. package/package.json +1 -1
  169. package/dist/dto/change-password-request.dto.d.ts +0 -43
  170. package/dist/dto/change-password-request.dto.d.ts.map +0 -1
  171. package/dist/dto/change-password-request.dto.js.map +0 -1
  172. package/dist/dto/update-user-attributes-request.dto.d.ts +0 -44
  173. package/dist/dto/update-user-attributes-request.dto.d.ts.map +0 -1
  174. package/dist/dto/update-user-attributes-request.dto.js.map +0 -1
@@ -38,34 +38,31 @@ const auth_audit_event_type_enum_1 = require("../enums/auth-audit-event-type.enu
38
38
  const risk_factor_enum_1 = require("../enums/risk-factor.enum");
39
39
  const context_storage_1 = require("../utils/context-storage");
40
40
  const signup_dto_1 = require("../dto/signup.dto");
41
- const admin_signup_dto_1 = require("../dto/admin-signup.dto");
42
- const admin_signup_social_dto_1 = require("../dto/admin-signup-social.dto");
41
+ // Admin DTOs moved to AdminAuthService
43
42
  const login_dto_1 = require("../dto/login.dto");
44
- const change_password_request_dto_1 = require("../dto/change-password-request.dto");
45
- const user_response_dto_1 = require("../dto/user-response.dto");
43
+ const change_password_dto_1 = require("../dto/change-password.dto");
44
+ const admin_update_user_attributes_dto_1 = require("../dto/admin-update-user-attributes.dto");
46
45
  const auth_response_dto_1 = require("../dto/auth-response.dto");
47
46
  const auth_challenge_dto_1 = require("../dto/auth-challenge.dto");
48
47
  const respond_challenge_dto_1 = require("../dto/respond-challenge.dto");
48
+ // Admin-only lookups are handled by AdminAuthService
49
49
  const logout_dto_1 = require("../dto/logout.dto");
50
50
  const logout_all_dto_1 = require("../dto/logout-all.dto");
51
- const get_user_sessions_dto_1 = require("../dto/get-user-sessions.dto");
52
51
  const logout_session_dto_1 = require("../dto/logout-session.dto");
53
52
  const refresh_token_dto_1 = require("../dto/refresh-token.dto");
54
53
  const resend_code_dto_1 = require("../dto/resend-code.dto");
55
- const admin_set_password_dto_1 = require("../dto/admin-set-password.dto");
56
- const admin_reset_password_dto_1 = require("../dto/admin-reset-password.dto");
54
+ const validate_access_token_dto_1 = require("../dto/validate-access-token.dto");
55
+ // Admin-only password workflows are handled by AdminAuthService
57
56
  const forgot_password_dto_1 = require("../dto/forgot-password.dto");
58
57
  const confirm_forgot_password_dto_1 = require("../dto/confirm-forgot-password.dto");
59
58
  const verify_email_dto_1 = require("../dto/verify-email.dto");
60
59
  const verify_phone_dto_1 = require("../dto/verify-phone.dto");
61
- const validate_access_token_dto_1 = require("../dto/validate-access-token.dto");
60
+ const admin_get_user_auth_history_dto_1 = require("../dto/admin-get-user-auth-history.dto");
62
61
  const auth_service_internal_helpers_1 = require("./auth-service-internal-helpers");
63
62
  const user_service_1 = require("./user.service");
64
63
  const nauth_exception_1 = require("../exceptions/nauth.exception");
65
64
  const error_codes_enum_1 = require("../enums/error-codes.enum");
66
- const class_validator_1 = require("class-validator");
67
65
  const crypto = __importStar(require("crypto"));
68
- const password_generator_1 = require("../utils/password-generator");
69
66
  const dto_validator_1 = require("../utils/dto-validator");
70
67
  const cookies_util_1 = require("../utils/cookies.util");
71
68
  /**
@@ -77,6 +74,25 @@ const cookies_util_1 = require("../utils/cookies.util");
77
74
  * Format: $argon2id$v=19$m=65536,t=3,p=4$salt$hash
78
75
  */
79
76
  const DUMMY_ARGON2_HASH = '$argon2id$v=19$m=65536,t=3,p=4$RFVNTVlfU0FMVF9GT1JfVElNSU5H$dummyhashfordummyhashfordummyhash1234567890';
77
+ /**
78
+ * Core user-facing authentication service
79
+ *
80
+ * This service implements **self-service** authentication flows for the currently authenticated user:
81
+ * - Signup, login, challenge completion, refresh token rotation
82
+ * - Logout / logout-all / logout-session (self-management)
83
+ * - Profile management and password change (self-management)
84
+ *
85
+ * Admin-only operations (explicit targeting via `sub`) are intentionally owned by {@link AdminAuthService}.
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * // Login (self-service)
90
+ * const result = await authService.login({ identifier: 'user@example.com', password: 'Password123!' });
91
+ *
92
+ * // Refresh (self-service; cookies or JSON depending on config)
93
+ * const refreshed = await authService.refreshToken({ refreshToken: '...' });
94
+ * ```
95
+ */
80
96
  class AuthService {
81
97
  userRepository;
82
98
  loginAttemptRepository;
@@ -374,590 +390,6 @@ class AuthService {
374
390
  return response;
375
391
  }
376
392
  // ============================================================================
377
- // Admin Signup
378
- // ============================================================================
379
- /**
380
- * Administrative user creation with override capabilities
381
- *
382
- * Allows administrators to create user accounts with:
383
- * - Bypass email/phone verification requirements
384
- * - Force password change on first login
385
- * - Auto-generate secure passwords
386
- *
387
- * Security:
388
- * - No built-in authentication - endpoint must be protected by framework adapter
389
- * - All duplicate checks still enforced
390
- * - Password policy still enforced (unless auto-generated)
391
- * - Audit trail records admin-created accounts
392
- *
393
- * @param dto - Admin signup DTO with override flags
394
- * @returns User object and optionally generated password
395
- * @throws {NAuthException} EMAIL_EXISTS | USERNAME_EXISTS | PHONE_EXISTS | WEAK_PASSWORD
396
- *
397
- * @example
398
- * ```typescript
399
- * // Create user with pre-verified email
400
- * const result = await authService.adminSignup({
401
- * email: 'user@example.com',
402
- * password: 'SecurePass123!',
403
- * isEmailVerified: true,
404
- * });
405
- *
406
- * // Create user with auto-generated password
407
- * const result = await authService.adminSignup({
408
- * email: 'user@example.com',
409
- * generatePassword: true,
410
- * isEmailVerified: true,
411
- * mustChangePassword: true,
412
- * });
413
- * // result.generatedPassword contains the temporary password
414
- * ```
415
- */
416
- async adminSignup(dto) {
417
- // Ensure DTO is validated (supports direct usage without framework validation)
418
- dto = await (0, dto_validator_1.ensureValidatedDto)(admin_signup_dto_1.AdminSignupDTO, dto);
419
- // Get client info from request context (transparent!)
420
- const clientInfo = this.clientInfoService.get();
421
- this.logger?.log?.(`Admin signup attempt for email: ${dto.email}`);
422
- this.logger?.debug?.(`Admin signup details: { email: ${dto.email}, username: ${dto.username || 'none'}, ip: ${clientInfo.ipAddress} }`);
423
- // Skip signup.enabled check (admin bypass)
424
- // Check if user already exists (email and username)
425
- this.logger?.debug?.(`Checking if user exists: ${dto.email}`);
426
- const existingUserByEmail = await this.userRepository.findOne({
427
- where: { email: dto.email },
428
- });
429
- if (existingUserByEmail) {
430
- this.logger?.warn?.(`Admin signup failed - user already exists: ${dto.email}`);
431
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
432
- }
433
- // Check for duplicate username if provided
434
- if (dto.username) {
435
- this.logger?.debug?.(`Checking if username exists: ${dto.username}`);
436
- const existingUserByUsername = await this.userRepository.findOne({
437
- where: { username: dto.username },
438
- });
439
- if (existingUserByUsername) {
440
- this.logger?.warn?.(`Admin signup failed - username already exists: ${dto.username}`);
441
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
442
- }
443
- }
444
- // Check for duplicate phone if provided and duplicates not allowed
445
- if (dto.phone && !this.config.signup?.allowDuplicatePhones) {
446
- this.logger?.debug?.(`Checking if phone exists: ${dto.phone}`);
447
- const existingUserByPhone = await this.userRepository.findOne({
448
- where: { phone: dto.phone },
449
- });
450
- if (existingUserByPhone) {
451
- this.logger?.warn?.(`Admin signup failed - phone already exists: ${dto.phone}`);
452
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
453
- }
454
- }
455
- // Handle password
456
- let passwordHash;
457
- let generatedPassword;
458
- if (dto.generatePassword) {
459
- // Generate secure random password
460
- generatedPassword = (0, password_generator_1.generateSecurePassword)(16);
461
- this.logger?.debug?.(`Generated password for admin-created user: ${dto.email}`);
462
- passwordHash = await this.passwordService.hashPassword(generatedPassword);
463
- }
464
- else {
465
- // Validate password policy
466
- if (!dto.password) {
467
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.WEAK_PASSWORD, 'Password is required when generatePassword is false');
468
- }
469
- this.logger?.debug?.('Validating password against policy');
470
- const passwordValidation = await this.passwordService.validatePassword(dto.password, {
471
- email: dto.email,
472
- username: dto.username,
473
- });
474
- if (!passwordValidation.valid) {
475
- this.logger?.warn?.(`Password validation failed for ${dto.email}: ${passwordValidation.errors.join(', ')}`);
476
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.WEAK_PASSWORD, passwordValidation.errors.join(', '), {
477
- errors: passwordValidation.errors,
478
- });
479
- }
480
- // Hash password
481
- passwordHash = await this.passwordService.hashPassword(dto.password);
482
- }
483
- // ============================================================================
484
- // Lifecycle Hook: preSignup
485
- // ============================================================================
486
- // Execute preSignup hook before user creation (admin signup)
487
- // Hook can throw NAuthException with PRESIGNUP_FAILED to block signup with custom message
488
- await this.hookRegistry.executePreSignup(dto, 'password', undefined, true);
489
- // Create user with override flags
490
- this.logger?.debug?.(`Creating admin user record for: ${dto.email} || ${dto.username} || ${dto.phone} (isEmailVerified: ${dto.isEmailVerified || false}, isPhoneVerified: ${dto.isPhoneVerified || false})`);
491
- const user = this.userRepository.create({
492
- email: dto.email,
493
- username: dto.username,
494
- firstName: dto.firstName,
495
- lastName: dto.lastName,
496
- phone: dto.phone,
497
- passwordHash,
498
- passwordChangedAt: new Date(),
499
- isEmailVerified: dto.isEmailVerified ?? false, // Use DTO value or default to false
500
- isPhoneVerified: dto.isPhoneVerified ?? false, // Use DTO value or default to false
501
- mustChangePassword: dto.mustChangePassword ?? false, // Use DTO value or default to false
502
- isActive: true, // Always active
503
- metadata: dto.metadata,
504
- });
505
- let savedUser;
506
- try {
507
- savedUser = (await this.userRepository.save(user));
508
- this.logger?.log?.(`Admin user created successfully: ${dto.email} (sub: ${savedUser.sub})`);
509
- // ============================================================================
510
- // Audit: Record account creation by admin
511
- // ============================================================================
512
- try {
513
- await this.auditService?.recordEvent({
514
- userId: savedUser.id,
515
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_CREATED,
516
- eventStatus: 'INFO',
517
- authMethod: 'admin',
518
- // Client info automatically included from context
519
- metadata: {
520
- email: savedUser.email,
521
- username: savedUser.username || null,
522
- createdByAdmin: true,
523
- adminIdentifier: clientInfo.ipAddress || 'unknown',
524
- isEmailVerified: savedUser.isEmailVerified,
525
- isPhoneVerified: savedUser.isPhoneVerified,
526
- mustChangePassword: savedUser.mustChangePassword,
527
- passwordGenerated: !!generatedPassword,
528
- },
529
- });
530
- }
531
- catch (auditError) {
532
- // Non-blocking: Log but continue
533
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
534
- this.logger?.error?.(`Failed to record ACCOUNT_CREATED audit event: ${errorMessage}`, {
535
- error: auditError,
536
- userId: savedUser.id,
537
- });
538
- }
539
- }
540
- catch (error) {
541
- // Handle database constraint violations gracefully
542
- if (error && typeof error === 'object' && 'code' in error && error.code === '23505') {
543
- // PostgreSQL unique constraint violation
544
- const dbError = error;
545
- if (dbError.detail?.includes('email')) {
546
- this.logger?.warn?.(`Admin signup failed - email constraint violation: ${dto.email}`);
547
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
548
- }
549
- else if (dbError.detail?.includes('username')) {
550
- this.logger?.warn?.(`Admin signup failed - username constraint violation: ${dto.username}`);
551
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
552
- }
553
- else if (dbError.detail?.includes('phone')) {
554
- this.logger?.warn?.(`Admin signup failed - phone constraint violation: ${dto.phone}`);
555
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
556
- }
557
- else {
558
- this.logger?.error?.(`Admin signup failed - database constraint violation: ${dbError.message}`);
559
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this information already exists', {
560
- conflictType: 'unknown',
561
- });
562
- }
563
- }
564
- // Re-throw other database errors
565
- const errorMessage = error instanceof Error ? error.message : 'Unknown database error';
566
- this.logger?.error?.(`Admin signup failed - database error: ${errorMessage}`);
567
- throw error;
568
- }
569
- // ============================================================================
570
- // Lifecycle Hook: afterSignup
571
- // ============================================================================
572
- // Execute afterSignup hook immediately after account creation (non-blocking)
573
- await this.hookRegistry.executePostSignup(savedUser, {
574
- signupType: 'password',
575
- adminSignup: true,
576
- });
577
- // No tokens, no challenge system, no verification emails - pure user creation
578
- // Return sanitized user object (excludes passwordHash and other sensitive fields)
579
- const userDto = user_response_dto_1.UserResponseDto.fromEntity(savedUser);
580
- return {
581
- user: userDto,
582
- generatedPassword,
583
- };
584
- }
585
- // ============================================================================
586
- // Admin Social Signup
587
- // ============================================================================
588
- /**
589
- * Administrative social user import with override capabilities
590
- *
591
- * Allows administrators to import existing social users from external platforms
592
- * (e.g., Cognito, Auth0) into nauth with:
593
- * - Bypass email/phone verification requirements
594
- * - Optional password for hybrid social+password accounts
595
- * - Social account linkage (provider + providerId)
596
- * - Automatic user flag updates (hasSocialAuth)
597
- *
598
- * Use case: Migrating users from external authentication platforms while
599
- * preserving their social login connections for transparent future logins.
600
- *
601
- * Security:
602
- * - No built-in authentication - endpoint must be protected by framework adapter
603
- * - All duplicate checks enforced (email, username, phone, provider+providerId)
604
- * - Password policy enforced if password provided
605
- * - Audit trail records admin-imported social accounts
606
- *
607
- * @param dto - Admin social signup DTO with social account details
608
- * @returns User object and social account confirmation
609
- * @throws {NAuthException} EMAIL_EXISTS | USERNAME_EXISTS | PHONE_EXISTS | SOCIAL_ACCOUNT_EXISTS | WEAK_PASSWORD
610
- *
611
- * @example
612
- * ```typescript
613
- * // Import social-only user from Cognito
614
- * // Note: Email is automatically verified for social imports (like normal social signup)
615
- * const result = await authService.adminSignupSocial({
616
- * email: 'user@example.com',
617
- * provider: 'google',
618
- * providerId: 'google_12345',
619
- * providerEmail: 'user@gmail.com',
620
- * socialMetadata: { sub: 'google_12345', given_name: 'John' },
621
- * });
622
- *
623
- * // Import hybrid user with password + social
624
- * const result = await authService.adminSignupSocial({
625
- * email: 'user@example.com',
626
- * password: 'SecurePass123!',
627
- * provider: 'apple',
628
- * providerId: 'apple_67890',
629
- * });
630
- * ```
631
- */
632
- async adminSignupSocial(dto) {
633
- // Ensure DTO is validated (supports direct usage without framework validation)
634
- dto = await (0, dto_validator_1.ensureValidatedDto)(admin_signup_social_dto_1.AdminSignupSocialDTO, dto);
635
- // Get client info from request context (transparent!)
636
- const clientInfo = this.clientInfoService.get();
637
- this.logger?.log?.(`Admin social signup attempt for email: ${dto.email}, provider: ${dto.provider}`);
638
- this.logger?.debug?.(`Admin social signup details: { email: ${dto.email}, username: ${dto.username || 'none'}, provider: ${dto.provider}, providerId: ${dto.providerId}, ip: ${clientInfo.ipAddress} }`);
639
- // Skip signup.enabled check (admin bypass)
640
- // Check if user already exists (email and username)
641
- this.logger?.debug?.(`Checking if user exists: ${dto.email}`);
642
- const existingUserByEmail = await this.userRepository.findOne({
643
- where: { email: dto.email },
644
- });
645
- if (existingUserByEmail) {
646
- this.logger?.warn?.(`Admin social signup failed - user already exists: ${dto.email}`);
647
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
648
- }
649
- // Check for duplicate username if provided
650
- if (dto.username) {
651
- this.logger?.debug?.(`Checking if username exists: ${dto.username}`);
652
- const existingUserByUsername = await this.userRepository.findOne({
653
- where: { username: dto.username },
654
- });
655
- if (existingUserByUsername) {
656
- this.logger?.warn?.(`Admin social signup failed - username already exists: ${dto.username}`);
657
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
658
- }
659
- }
660
- // Check for duplicate phone if provided and duplicates not allowed
661
- if (dto.phone && !this.config.signup?.allowDuplicatePhones) {
662
- this.logger?.debug?.(`Checking if phone exists: ${dto.phone}`);
663
- const existingUserByPhone = await this.userRepository.findOne({
664
- where: { phone: dto.phone },
665
- });
666
- if (existingUserByPhone) {
667
- this.logger?.warn?.(`Admin social signup failed - phone already exists: ${dto.phone}`);
668
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
669
- }
670
- }
671
- // Check for duplicate provider+providerId
672
- if (!this.socialAuthService) {
673
- this.logger?.error?.('SocialAuthService not available - cannot import social user');
674
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SOCIAL_CONFIG_MISSING, 'Social authentication is not configured');
675
- }
676
- this.logger?.debug?.(`Checking if social account exists: ${dto.provider}:${dto.providerId}`);
677
- const existingSocialAccount = await this.socialAuthService.findSocialAccountByProvider(dto.provider, dto.providerId);
678
- if (existingSocialAccount) {
679
- this.logger?.warn?.(`Admin social signup failed - social account already exists: ${dto.provider}:${dto.providerId}`);
680
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SOCIAL_ACCOUNT_EXISTS, 'This social account is already registered');
681
- }
682
- // Handle password (optional for hybrid accounts)
683
- let passwordHash;
684
- if (dto.password) {
685
- // Validate password policy
686
- this.logger?.debug?.('Validating password against policy');
687
- const passwordValidation = await this.passwordService.validatePassword(dto.password, {
688
- email: dto.email,
689
- username: dto.username,
690
- });
691
- if (!passwordValidation.valid) {
692
- this.logger?.warn?.(`Password validation failed for ${dto.email}: ${passwordValidation.errors.join(', ')}`);
693
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.WEAK_PASSWORD, passwordValidation.errors.join(', '), {
694
- errors: passwordValidation.errors,
695
- });
696
- }
697
- // Hash password
698
- passwordHash = await this.passwordService.hashPassword(dto.password);
699
- }
700
- else {
701
- // Social-only user: no password (NULL in database)
702
- passwordHash = null;
703
- }
704
- // ============================================================================
705
- // Lifecycle Hook: preSignup
706
- // ============================================================================
707
- // Execute preSignup hook before user creation (admin social signup)
708
- // Hook can throw NAuthException with PRESIGNUP_FAILED to block signup with custom message
709
- // Convert AdminSignupSocialDTO to profile-like structure for hook
710
- const profileData = {
711
- email: dto.email,
712
- id: dto.providerId,
713
- firstName: dto.firstName,
714
- lastName: dto.lastName,
715
- verified: true, // Admin signup always has verified email
716
- raw: dto.socialMetadata,
717
- };
718
- await this.hookRegistry.executePreSignup(profileData, 'social', dto.provider, true);
719
- // Create user with override flags
720
- // Note: Email is always verified for social imports (like normal social signup)
721
- this.logger?.debug?.(`Creating admin social user record for: ${dto.email} || ${dto.username} || ${dto.phone} (isEmailVerified: true [auto-verified for social], isPhoneVerified: ${dto.isPhoneVerified || false})`);
722
- const user = this.userRepository.create({
723
- email: dto.email,
724
- username: dto.username,
725
- firstName: dto.firstName,
726
- lastName: dto.lastName,
727
- phone: dto.phone,
728
- passwordHash, // null for social-only, hashed string for hybrid accounts
729
- passwordChangedAt: dto.password ? new Date() : null, // Only set if password provided
730
- isEmailVerified: true, // Always verified for social imports (like normal social signup)
731
- isPhoneVerified: dto.isPhoneVerified ?? false, // Use DTO value or default to false
732
- mustChangePassword: dto.mustChangePassword ?? false, // Use DTO value or default to false
733
- isActive: true, // Always active
734
- metadata: dto.metadata,
735
- hasSocialAuth: true, // Set immediately since we know this is a social user
736
- socialProviders: [dto.provider], // Set immediately with the provider from DTO
737
- });
738
- let savedUser;
739
- try {
740
- savedUser = (await this.userRepository.save(user));
741
- this.logger?.log?.(`Admin social user created successfully: ${dto.email} (sub: ${savedUser.sub}, provider: ${dto.provider})`);
742
- // Create social account linkage
743
- this.logger?.debug?.(`Creating social account linkage: ${dto.provider}:${dto.providerId}`);
744
- await this.socialAuthService.createOrUpdateSocialAccount(savedUser.id, dto.provider, dto.providerId, dto.providerEmail || null, dto.socialMetadata);
745
- this.logger?.log?.(`Social account linked successfully: ${dto.provider}:${dto.providerId}`);
746
- // Update savedUser in memory to reflect the updated social flags (no additional query needed)
747
- // updateUserSocialFlags() has already updated the DB, we just sync the in-memory object
748
- savedUser.hasSocialAuth = true;
749
- savedUser.socialProviders = [dto.provider];
750
- // ============================================================================
751
- // Lifecycle Hook: afterSignup
752
- // ============================================================================
753
- // Execute afterSignup hook immediately after account creation (non-blocking)
754
- // Extract profile picture from social metadata if available
755
- const profilePicture = dto.socialMetadata && typeof dto.socialMetadata === 'object' && 'picture' in dto.socialMetadata
756
- ? dto.socialMetadata.picture
757
- : null;
758
- await this.hookRegistry.executePostSignup(savedUser, {
759
- signupType: 'social',
760
- provider: dto.provider,
761
- adminSignup: true,
762
- socialMetadata: dto.socialMetadata || null,
763
- profilePicture,
764
- });
765
- // ============================================================================
766
- // Audit: Record account creation by admin (social import)
767
- // ============================================================================
768
- try {
769
- await this.auditService?.recordEvent({
770
- userId: savedUser.id,
771
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_CREATED,
772
- eventStatus: 'INFO',
773
- authMethod: 'admin-social',
774
- // Client info automatically included from context
775
- metadata: {
776
- email: savedUser.email,
777
- username: savedUser.username || null,
778
- createdByAdmin: true,
779
- adminIdentifier: clientInfo.ipAddress || 'unknown',
780
- isEmailVerified: savedUser.isEmailVerified,
781
- isPhoneVerified: savedUser.isPhoneVerified,
782
- mustChangePassword: savedUser.mustChangePassword,
783
- provider: dto.provider,
784
- providerId: dto.providerId,
785
- hasPassword: !!dto.password,
786
- socialImport: true,
787
- },
788
- });
789
- }
790
- catch (auditError) {
791
- // Non-blocking: Log but continue
792
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
793
- this.logger?.error?.(`Failed to record ACCOUNT_CREATED audit event: ${errorMessage}`, {
794
- error: auditError,
795
- userId: savedUser.id,
796
- });
797
- }
798
- }
799
- catch (error) {
800
- // Handle database constraint violations gracefully
801
- if (error && typeof error === 'object' && 'code' in error && error.code === '23505') {
802
- // PostgreSQL unique constraint violation
803
- const dbError = error;
804
- if (dbError.detail?.includes('email')) {
805
- this.logger?.warn?.(`Admin social signup failed - email constraint violation: ${dto.email}`);
806
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
807
- }
808
- else if (dbError.detail?.includes('username')) {
809
- this.logger?.warn?.(`Admin social signup failed - username constraint violation: ${dto.username}`);
810
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
811
- }
812
- else if (dbError.detail?.includes('phone')) {
813
- this.logger?.warn?.(`Admin social signup failed - phone constraint violation: ${dto.phone}`);
814
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
815
- }
816
- else if (dbError.detail?.includes('provider') && dbError.detail?.includes('providerId')) {
817
- this.logger?.warn?.(`Admin social signup failed - social account constraint violation: ${dto.provider}:${dto.providerId}`);
818
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SOCIAL_ACCOUNT_EXISTS, 'This social account is already registered');
819
- }
820
- else {
821
- this.logger?.error?.(`Admin social signup failed - database constraint violation: ${dbError.message}`);
822
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this information already exists', {
823
- conflictType: 'unknown',
824
- });
825
- }
826
- }
827
- // Re-throw other database errors
828
- const errorMessage = error instanceof Error ? error.message : 'Unknown database error';
829
- this.logger?.error?.(`Admin social signup failed - database error: ${errorMessage}`);
830
- throw error;
831
- }
832
- // No tokens, no challenge system, no verification emails - pure user creation with social linkage
833
- // Return sanitized user object and social account confirmation
834
- const userDto = user_response_dto_1.UserResponseDto.fromEntity(savedUser);
835
- return {
836
- user: userDto,
837
- socialAccount: {
838
- provider: dto.provider,
839
- providerId: dto.providerId,
840
- providerEmail: dto.providerEmail || null,
841
- },
842
- };
843
- }
844
- // ============================================================================
845
- // Admin User Management
846
- // ============================================================================
847
- /**
848
- * Administrative user deletion with complete cascade cleanup
849
- *
850
- * HARD DELETE - Permanently removes user and ALL associated data including:
851
- * - Sessions, verification tokens, MFA devices, trusted devices
852
- * - Social accounts, login attempts, challenge sessions, audit logs
853
- *
854
- * Security:
855
- * - NO built-in authentication - endpoint MUST be protected by admin guards
856
- * - Records admin action in separate audit log (not deleted with user)
857
- * - Irreversible operation - all data permanently removed
858
- *
859
- * @param dto - User sub to delete
860
- * @returns Deletion confirmation with cascade counts
861
- * @throws {NAuthException} USER_NOT_FOUND
862
- *
863
- * @example
864
- * ```typescript
865
- * const result = await authService.deleteUser({ sub: 'user-uuid-123' });
866
- * console.log(`Deleted user: ${result.deletedUserId}`);
867
- * console.log(`Deleted ${result.deletedRecords.sessions} sessions`);
868
- * ```
869
- */
870
- async deleteUser(dto) {
871
- return await this.userService.deleteUser(dto);
872
- }
873
- /**
874
- * Get paginated list of users with advanced filtering
875
- *
876
- * Supports pagination, boolean filters, exact match filters,
877
- * date filters with operators (gt, gte, lt, lte, eq), and flexible sorting.
878
- *
879
- * Security:
880
- * - NO built-in authentication - endpoint MUST be protected by admin guards
881
- * - Returns sanitized user data (no passwordHash, secrets)
882
- *
883
- * @param dto - Filters, pagination, sorting
884
- * @returns Paginated user list with metadata
885
- *
886
- * @example
887
- * ```typescript
888
- * const result = await authService.getUsers({
889
- * page: 1,
890
- * limit: 20,
891
- * isEmailVerified: true,
892
- * hasSocialAuth: true,
893
- * createdAt: { operator: 'gte', value: new Date('2024-01-01') },
894
- * sortBy: 'createdAt',
895
- * sortOrder: 'DESC'
896
- * });
897
- * ```
898
- */
899
- async getUsers(dto) {
900
- return await this.userService.getUsers(dto);
901
- }
902
- /**
903
- * Administrative permanent account locking
904
- *
905
- * Sets permanent lock (lockedUntil=NULL) and immediately revokes all active sessions.
906
- * Reuses existing rate-limit lock fields (isLocked, lockReason, lockedAt, lockedUntil).
907
- *
908
- * Permanent vs Temporary locks:
909
- * - Rate limiting: lockedUntil = future date (temporary auto-unlock)
910
- * - Admin disableUser: lockedUntil = NULL (permanent manual lock)
911
- *
912
- * Security:
913
- * - NO built-in authentication - endpoint MUST be protected by admin guards
914
- * - Revokes all sessions immediately (forced logout)
915
- * - Records ACCOUNT_DISABLED audit event with admin identifier
916
- *
917
- * @param dto - User sub and optional reason
918
- * @returns User object with updated lock status and revoked session count
919
- * @throws {NAuthException} USER_NOT_FOUND
920
- *
921
- * @example
922
- * ```typescript
923
- * const result = await authService.disableUser({
924
- * sub: 'user-uuid-123',
925
- * reason: 'Suspicious activity detected'
926
- * });
927
- * console.log(`Revoked ${result.revokedSessions} sessions`);
928
- * ```
929
- */
930
- async disableUser(dto) {
931
- return await this.userService.disableUser(dto);
932
- }
933
- /**
934
- * Enable (unlock) user account
935
- *
936
- * Unlocks a previously locked user account by clearing all lock fields.
937
- * This reverses the effect of disableUser() or rate-limit lockouts.
938
- *
939
- * Security:
940
- * - NO built-in authentication - endpoint MUST be protected by admin guards
941
- * - Clears lock fields (isLocked, lockReason, lockedAt, lockedUntil)
942
- * - Resets failed login attempts counter
943
- * - Records ACCOUNT_ENABLED audit event with admin identifier
944
- *
945
- * @param dto - User sub to enable
946
- * @returns User object with updated lock status
947
- * @throws {NAuthException} USER_NOT_FOUND
948
- *
949
- * @example
950
- * ```typescript
951
- * const result = await authService.enableUser({
952
- * sub: 'user-uuid-123'
953
- * });
954
- * console.log(`User unlocked: ${result.user.email}`);
955
- * ```
956
- */
957
- async enableUser(dto) {
958
- return await this.userService.enableUser(dto);
959
- }
960
- // ============================================================================
961
393
  // User Login
962
394
  // ============================================================================
963
395
  /**
@@ -1002,7 +434,7 @@ class AuthService {
1002
434
  // ============================================================================
1003
435
  // We do not have a resolved user yet because IP lockout happens before the normal
1004
436
  // identifier validation + user lookup. Resolve it here to avoid passing a non-UUID
1005
- // (email/username/phone) into `userSub`.
437
+ // (email/username/phone) into a `sub` field.
1006
438
  const userForAudit = await this.helpers.findUserByIdentifier(dto.identifier, identifierType);
1007
439
  if (fireAndForget) {
1008
440
  if (userForAudit?.id) {
@@ -1100,7 +532,7 @@ class AuthService {
1100
532
  this.logger?.error?.(`Failed to record LOGIN_FAILED audit event (fire-and-forget): ${errorMessage}`, {
1101
533
  error: err,
1102
534
  userId: user.id,
1103
- userSub: user.sub,
535
+ sub: user.sub,
1104
536
  });
1105
537
  });
1106
538
  }
@@ -1171,7 +603,7 @@ class AuthService {
1171
603
  this.logger?.error?.(`Failed to record LOGIN_BLOCKED audit event (fire-and-forget): ${errorMessage}`, {
1172
604
  error: err,
1173
605
  userId: user.id,
1174
- userSub: user.sub,
606
+ sub: user.sub,
1175
607
  });
1176
608
  });
1177
609
  }
@@ -1354,7 +786,7 @@ class AuthService {
1354
786
  this.logger?.error?.(`Failed to record LOGIN_SUCCESS audit event (fire-and-forget): ${errorMessage}`, {
1355
787
  error: err,
1356
788
  userId: user.id,
1357
- userSub: user.sub,
789
+ sub: user.sub,
1358
790
  });
1359
791
  });
1360
792
  }
@@ -1540,7 +972,7 @@ class AuthService {
1540
972
  this.logger?.error?.(`Failed to record LOGIN_SUCCESS audit event (fire-and-forget): ${errorMessage}`, {
1541
973
  error: err,
1542
974
  userId: user.id,
1543
- userSub: user.sub,
975
+ sub: user.sub,
1544
976
  });
1545
977
  });
1546
978
  }
@@ -1774,7 +1206,7 @@ class AuthService {
1774
1206
  if (!provider.sendChallenge) {
1775
1207
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `${method.toUpperCase()} MFA provider does not support sending challenges`);
1776
1208
  }
1777
- const result = await provider.sendChallenge(user);
1209
+ const result = await provider.sendChallenge?.();
1778
1210
  this.logger?.debug?.(`${method.toUpperCase()} MFA code resent: user=${user.sub}`);
1779
1211
  // Provider returns masked phone or email
1780
1212
  return { destination: result };
@@ -1942,238 +1374,255 @@ class AuthService {
1942
1374
  * ```
1943
1375
  */
1944
1376
  async refreshToken(dto) {
1945
- // Ensure DTO is validated (supports direct usage without framework validation)
1946
- dto = await (0, dto_validator_1.ensureValidatedDto)(refresh_token_dto_1.RefreshTokenDTO, dto);
1947
- // After validation, refreshToken must be present (validation ensures it's a valid string)
1948
- // Controller should have filled it from cookies if it was missing in cookies mode
1949
- if (!dto.refreshToken) {
1950
- // Best-effort: clear cookies in cookie/hybrid delivery so clients don't keep sending stale cookies.
1951
- this.clearAuthCookiesOnRefreshFailure(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID);
1952
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Refresh token is required');
1953
- }
1954
- // Extract to const for type narrowing (TypeScript doesn't narrow optional properties)
1955
- const refreshToken = dto.refreshToken;
1956
- const tokenHash = this.jwtService.hashToken(refreshToken);
1957
- // ============================================================================
1958
- // CRITICAL SECURITY FIX #1 & #2: Distributed Lock + Reuse Detection
1959
- // ============================================================================
1960
- // CRITICAL: We need to get session ID for locking, but we must lock BEFORE validation
1961
- // to prevent race conditions. So we do a quick, lightweight lookup first.
1962
- // Find session by refresh token hash - this is fast and allows us to get session ID
1963
- const session = await this.sessionService.findByRefreshToken(tokenHash);
1964
- if (!session || session.isRevoked) {
1965
- // Validate token to get user info for error message
1966
- const validation = await this.jwtService.validateRefreshToken(refreshToken);
1967
- const userId = validation.payload?.sub || 'unknown';
1968
- this.logger?.debug?.(`Session not found or revoked for user ${userId}. Possible issue where token are not cleared on logout`);
1969
- // Best-effort: clear cookies in cookie/hybrid delivery so clients don't keep sending stale cookies.
1970
- this.clearAuthCookiesOnRefreshFailure(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND);
1971
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1972
- }
1973
- // Acquire distributed lock using SESSION ID (not token hash)
1974
- // THIS MUST HAPPEN BEFORE VALIDATION to prevent race conditions
1975
- // where multiple requests validate the same token before any lock is acquired
1976
- const lockKey = `session-refresh:${session.id}`;
1977
- this.logger?.debug?.(`[REFRESH DEBUG] Attempting to acquire lock ${lockKey} for token hash ${tokenHash.substring(0, 16)}...`);
1978
- let lockAcquired = false;
1979
1377
  try {
1980
- const lockStartTime = Date.now();
1981
- lockAcquired = await this.sessionService.acquireRefreshLock(lockKey, 10000);
1982
- const lockDuration = Date.now() - lockStartTime;
1983
- if (!lockAcquired) {
1984
- this.logger?.warn?.(`[REFRESH DEBUG] Lock ${lockKey} NOT acquired - refresh already in progress for session ${session.id}`);
1985
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.RATE_LIMIT_LOGIN, 'Token refresh already in progress', {
1986
- retryAfter: 5,
1987
- });
1378
+ // Ensure DTO is validated (supports direct usage without framework validation)
1379
+ dto = await (0, dto_validator_1.ensureValidatedDto)(refresh_token_dto_1.RefreshTokenDTO, dto);
1380
+ // After validation, refreshToken must be present (validation ensures it's a valid string)
1381
+ // Controller should have filled it from cookies if it was missing in cookies mode
1382
+ if (!dto.refreshToken) {
1383
+ // Best-effort: clear cookies in cookie/hybrid delivery so clients don't keep sending stale cookies.
1384
+ this.clearAuthCookiesOnRefreshFailure(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID);
1385
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Refresh token is required');
1386
+ }
1387
+ // Extract to const for type narrowing (TypeScript doesn't narrow optional properties)
1388
+ const refreshToken = dto.refreshToken;
1389
+ const tokenHash = this.jwtService.hashToken(refreshToken);
1390
+ // ============================================================================
1391
+ // CRITICAL SECURITY FIX #1 & #2: Distributed Lock + Reuse Detection
1392
+ // ============================================================================
1393
+ // CRITICAL: We need to get session ID for locking, but we must lock BEFORE validation
1394
+ // to prevent race conditions. So we do a quick, lightweight lookup first.
1395
+ // Find session by refresh token hash - this is fast and allows us to get session ID
1396
+ const session = await this.sessionService.findByRefreshToken(tokenHash);
1397
+ if (!session || session.isRevoked) {
1398
+ // Validate token to get user info for error message
1399
+ const validation = await this.jwtService.validateRefreshToken(refreshToken);
1400
+ const userId = validation.payload?.sub || 'unknown';
1401
+ this.logger?.debug?.(`Session not found or revoked for user ${userId}. Possible issue where token are not cleared on logout`);
1402
+ // Best-effort: clear cookies in cookie/hybrid delivery so clients don't keep sending stale cookies.
1403
+ this.clearAuthCookiesOnRefreshFailure(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND);
1404
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1988
1405
  }
1989
- this.logger?.debug?.(`[REFRESH DEBUG] Lock ${lockKey} acquired successfully in ${lockDuration}ms for token hash ${tokenHash.substring(0, 16)}...`);
1990
- // CRITICAL: Check for token reuse IMMEDIATELY after acquiring lock
1991
- // If same session + cookie race return current tokens (don't reissue)
1992
- // If different session → invalidate that session and reject (attack)
1993
- if (this.config.jwt.refreshToken.reuseDetection) {
1994
- const isAlreadyUsed = await this.sessionService.isRefreshTokenUsed(tokenHash);
1995
- if (isAlreadyUsed) {
1996
- // Decode token to get sessionId from JWT payload (without full validation)
1997
- // This allows us to check if the token belongs to the session we found
1998
- const tokenPayload = this.jwtService.decodeToken(refreshToken);
1999
- const tokenSessionId = tokenPayload?.sessionId;
2000
- // Get current session state to ensure it's still valid
2001
- const currentSession = (await this.sessionService.findByIdLight(session.id));
2002
- if (!currentSession || currentSession.isRevoked) {
2003
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
2004
- }
2005
- // Check if token's sessionId matches the session we found
2006
- // If they match cookie race (same session)
2007
- // If they don't matchattack (token stolen from different session)
2008
- if (tokenSessionId && tokenSessionId === session.id.toString()) {
2009
- // Same session - this is a cookie race condition
2010
- // Return the current valid tokens (user already has them from first request)
2011
- this.logger?.debug?.(`[REFRESH DEBUG] Token hash ${tokenHash.substring(0, 16)}... already used for same session ${session.id} - cookie race detected, returning current tokens`);
2012
- // Get user info
2013
- const user = (await this.userRepository.findOne({
2014
- where: { id: currentSession.userId },
2015
- }));
2016
- if (!user) {
2017
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
1406
+ // Acquire distributed lock using SESSION ID (not token hash)
1407
+ // THIS MUST HAPPEN BEFORE VALIDATION to prevent race conditions
1408
+ // where multiple requests validate the same token before any lock is acquired
1409
+ const lockKey = `session-refresh:${session.id}`;
1410
+ this.logger?.debug?.(`[REFRESH DEBUG] Attempting to acquire lock ${lockKey} for token hash ${tokenHash.substring(0, 16)}...`);
1411
+ let lockAcquired = false;
1412
+ try {
1413
+ const lockStartTime = Date.now();
1414
+ lockAcquired = await this.sessionService.acquireRefreshLock(lockKey, 10000);
1415
+ const lockDuration = Date.now() - lockStartTime;
1416
+ if (!lockAcquired) {
1417
+ this.logger?.warn?.(`[REFRESH DEBUG] Lock ${lockKey} NOT acquired - refresh already in progress for session ${session.id}`);
1418
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.RATE_LIMIT_LOGIN, 'Token refresh already in progress', {
1419
+ retryAfter: 5,
1420
+ });
1421
+ }
1422
+ this.logger?.debug?.(`[REFRESH DEBUG] Lock ${lockKey} acquired successfully in ${lockDuration}ms for token hash ${tokenHash.substring(0, 16)}...`);
1423
+ // CRITICAL: Check for token reuse IMMEDIATELY after acquiring lock
1424
+ // If same session + cookie race return current tokens (don't reissue)
1425
+ // If different session invalidate that session and reject (attack)
1426
+ if (this.config.jwt.refreshToken.reuseDetection) {
1427
+ const isAlreadyUsed = await this.sessionService.isRefreshTokenUsed(tokenHash);
1428
+ if (isAlreadyUsed) {
1429
+ // Decode token to get sessionId from JWT payload (without full validation)
1430
+ // This allows us to check if the token belongs to the session we found
1431
+ const tokenPayload = this.jwtService.decodeToken(refreshToken);
1432
+ const tokenSessionId = tokenPayload?.sessionId;
1433
+ // Get current session state to ensure it's still valid
1434
+ const currentSession = (await this.sessionService.findByIdLight(session.id));
1435
+ if (!currentSession || currentSession.isRevoked) {
1436
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1437
+ }
1438
+ // Check if token's sessionId matches the session we found
1439
+ // If they match → cookie race (same session)
1440
+ // If they don't match → attack (token stolen from different session)
1441
+ if (tokenSessionId && tokenSessionId === session.id.toString()) {
1442
+ // Same session - this is a cookie race condition
1443
+ // Return the current valid tokens (user already has them from first request)
1444
+ this.logger?.debug?.(`[REFRESH DEBUG] Token hash ${tokenHash.substring(0, 16)}... already used for same session ${session.id} - cookie race detected, returning current tokens`);
1445
+ // Get user info
1446
+ const user = (await this.userRepository.findOne({
1447
+ where: { id: currentSession.userId },
1448
+ }));
1449
+ if (!user) {
1450
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
1451
+ }
1452
+ // Generate tokens from current session state (same as what the first request returned)
1453
+ // These will match what the user already has, so no change needed
1454
+ // Note: deviceId not included in token - session.deviceId is source of truth
1455
+ const newTokens = await this.jwtService.generateTokenPair({
1456
+ userId: user.sub,
1457
+ email: user.email,
1458
+ sessionId: currentSession.id.toString(),
1459
+ tokenFamily: currentSession.tokenFamily,
1460
+ });
1461
+ // Update session with these tokens (they're already there, but ensures consistency)
1462
+ await this.sessionService.updateTokens(currentSession.id, this.jwtService.hashToken(newTokens.accessToken), this.jwtService.hashToken(newTokens.refreshToken));
1463
+ // Decode tokens to get expiry times
1464
+ const accessTokenValidation = await this.jwtService.validateAccessToken(newTokens.accessToken);
1465
+ const refreshTokenValidation = await this.jwtService.validateRefreshToken(newTokens.refreshToken);
1466
+ // Return success with current tokens
1467
+ return {
1468
+ accessToken: newTokens.accessToken,
1469
+ refreshToken: newTokens.refreshToken,
1470
+ accessTokenExpiresAt: accessTokenValidation.payload?.exp || 0,
1471
+ refreshTokenExpiresAt: refreshTokenValidation.payload?.exp || 0,
1472
+ };
1473
+ }
1474
+ else {
1475
+ // Different session - this is an attack!
1476
+ // A refresh token from one session cannot be used by another session
1477
+ this.logger?.error?.(`[REFRESH DEBUG] Token hash ${tokenHash.substring(0, 16)}... already used for different session - ATTACK DETECTED! Token sessionId: ${tokenSessionId}, Found session: ${session.id}. Revoking session ${session.id}`);
1478
+ // Revoke the session that's trying to use a stolen token
1479
+ await this.sessionService.revokeSession(session.id, 'Token reuse detected - possible token theft');
1480
+ // Audit the attack
1481
+ let userForAudit = null;
1482
+ try {
1483
+ userForAudit = (await this.userRepository.findOne({
1484
+ where: { id: session.userId },
1485
+ }));
1486
+ if (userForAudit) {
1487
+ await this.auditService?.recordEvent({
1488
+ userId: userForAudit.id,
1489
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.SUSPICIOUS_ACTIVITY,
1490
+ eventStatus: 'SUSPICIOUS',
1491
+ riskFactor: 90,
1492
+ riskFactors: [risk_factor_enum_1.RiskFactor.TOKEN_THEFT_ATTEMPT, risk_factor_enum_1.RiskFactor.REFRESH_TOKEN_REUSE_DIFFERENT_SESSION],
1493
+ reason: 'Refresh token reuse from different session',
1494
+ // Client info automatically included from context
1495
+ description: 'Refresh token from another session attempted to be used. Session revoked as security measure.',
1496
+ metadata: {
1497
+ sessionId: session.id,
1498
+ tokenSessionId,
1499
+ tokenHash: `${tokenHash.substring(0, 16)}...`,
1500
+ detectedAt: new Date().toISOString(),
1501
+ action: 'session_revoked',
1502
+ },
1503
+ });
1504
+ }
1505
+ }
1506
+ catch (auditError) {
1507
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
1508
+ this.logger?.error?.(`Failed to record SUSPICIOUS_ACTIVITY audit event (token reuse): ${errorMessage}`, {
1509
+ error: auditError,
1510
+ userId: userForAudit?.id || session.userId,
1511
+ });
1512
+ }
1513
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Refresh token has already been used');
2018
1514
  }
2019
- // Generate tokens from current session state (same as what the first request returned)
2020
- // These will match what the user already has, so no change needed
2021
- // Note: deviceId not included in token - session.deviceId is source of truth
2022
- const newTokens = await this.jwtService.generateTokenPair({
2023
- userId: user.sub,
2024
- email: user.email,
2025
- sessionId: currentSession.id.toString(),
2026
- tokenFamily: currentSession.tokenFamily,
2027
- });
2028
- // Update session with these tokens (they're already there, but ensures consistency)
2029
- await this.sessionService.updateTokens(currentSession.id, this.jwtService.hashToken(newTokens.accessToken), this.jwtService.hashToken(newTokens.refreshToken));
2030
- // Decode tokens to get expiry times
2031
- const accessTokenValidation = await this.jwtService.validateAccessToken(newTokens.accessToken);
2032
- const refreshTokenValidation = await this.jwtService.validateRefreshToken(newTokens.refreshToken);
2033
- // Return success with current tokens
2034
- return {
2035
- accessToken: newTokens.accessToken,
2036
- refreshToken: newTokens.refreshToken,
2037
- accessTokenExpiresAt: accessTokenValidation.payload?.exp || 0,
2038
- refreshTokenExpiresAt: refreshTokenValidation.payload?.exp || 0,
2039
- };
2040
1515
  }
2041
- else {
2042
- // Different session - this is an attack!
2043
- // A refresh token from one session cannot be used by another session
2044
- this.logger?.error?.(`[REFRESH DEBUG] Token hash ${tokenHash.substring(0, 16)}... already used for different session - ATTACK DETECTED! Token sessionId: ${tokenSessionId}, Found session: ${session.id}. Revoking session ${session.id}`);
2045
- // Revoke the session that's trying to use a stolen token
2046
- await this.sessionService.revokeSession(session.id, 'Token reuse detected - possible token theft');
2047
- // Audit the attack
2048
- let userForAudit = null;
1516
+ }
1517
+ // NOW validate the refresh token (after lock is acquired and reuse check)
1518
+ // This ensures only one request can validate at a time per session
1519
+ const validation = await this.jwtService.validateRefreshToken(refreshToken);
1520
+ if (!validation.valid || !validation.payload) {
1521
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Invalid refresh token');
1522
+ }
1523
+ const payload = validation.payload;
1524
+ // Re-check session after acquiring lock (it might have been revoked/updated)
1525
+ // Since we have the lock, no other request can modify this session, but it might have been revoked
1526
+ // We already have currentSession from the early reuse check, but re-fetch to ensure it's still valid
1527
+ const lockedSession = (await this.sessionService.findByIdLight(session.id));
1528
+ if (!lockedSession || lockedSession.isRevoked || lockedSession.id !== session.id) {
1529
+ this.logger?.debug?.(`Session changed after lock acquisition for user ${payload.sub}. Session may have been revoked.`);
1530
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1531
+ }
1532
+ // ============================================================================
1533
+ // NOTE: We still do the atomic mark operation below as a double-check
1534
+ // The early check above handles cookie race conditions where old tokens
1535
+ // are sent before new cookies are received
1536
+ // ============================================================================
1537
+ // Mark token as used BEFORE generating new tokens (prevents reuse)
1538
+ if (this.config.jwt.refreshToken.reuseDetection) {
1539
+ const refreshTokenTTL = this.jwtService.getRefreshTokenTTL();
1540
+ const marked = await this.sessionService.markRefreshTokenAsUsed(tokenHash, refreshTokenTTL);
1541
+ if (!marked) {
1542
+ // Token was already marked as used - reuse detected!
1543
+ this.logger?.error?.(`Token reuse detected for user ${payload.sub} - atomic mark failed, revoking entire token family ${payload.tokenFamily}`);
1544
+ // Audit the reuse attempt
2049
1545
  try {
2050
- userForAudit = (await this.userRepository.findOne({
2051
- where: { id: session.userId },
1546
+ const userForAudit = (await this.userRepository.findOne({
1547
+ where: { sub: payload.sub },
2052
1548
  }));
2053
1549
  if (userForAudit) {
2054
1550
  await this.auditService?.recordEvent({
2055
1551
  userId: userForAudit.id,
2056
1552
  eventType: auth_audit_event_type_enum_1.AuthAuditEventType.SUSPICIOUS_ACTIVITY,
2057
1553
  eventStatus: 'SUSPICIOUS',
2058
- riskFactor: 90,
2059
- riskFactors: [risk_factor_enum_1.RiskFactor.TOKEN_THEFT_ATTEMPT, risk_factor_enum_1.RiskFactor.REFRESH_TOKEN_REUSE_DIFFERENT_SESSION],
2060
- reason: 'Refresh token reuse from different session',
1554
+ riskFactor: 75,
1555
+ riskFactors: [risk_factor_enum_1.RiskFactor.TOKEN_REUSE_ATTEMPT],
1556
+ reason: 'Token reuse attempt blocked',
2061
1557
  // Client info automatically included from context
2062
- description: 'Refresh token from another session attempted to be used. Session revoked as security measure.',
1558
+ description: 'Refresh token reuse attempt detected via atomic operation. Legitimate user session preserved.',
2063
1559
  metadata: {
2064
- sessionId: session.id,
2065
- tokenSessionId,
2066
- tokenHash: `${tokenHash.substring(0, 16)}...`,
1560
+ tokenFamily: payload.tokenFamily,
2067
1561
  detectedAt: new Date().toISOString(),
2068
- action: 'session_revoked',
1562
+ action: 'reuse_blocked_atomic',
2069
1563
  },
2070
1564
  });
2071
1565
  }
2072
1566
  }
2073
1567
  catch (auditError) {
2074
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
2075
- this.logger?.error?.(`Failed to record SUSPICIOUS_ACTIVITY audit event (token reuse): ${errorMessage}`, {
2076
- error: auditError,
2077
- userId: userForAudit?.id || session.userId,
2078
- });
1568
+ this.logger?.warn?.('Failed to record SUSPICIOUS_ACTIVITY audit event', { error: auditError });
2079
1569
  }
2080
1570
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Refresh token has already been used');
2081
1571
  }
1572
+ this.logger?.debug?.(`Marked refresh token as used for session ${lockedSession.id}`);
2082
1573
  }
1574
+ // Generate new token pair with same family
1575
+ // Note: deviceId not included in token - session.deviceId is source of truth
1576
+ const newTokens = await this.jwtService.generateTokenPair({
1577
+ userId: payload.sub,
1578
+ email: payload.email,
1579
+ sessionId: lockedSession.id.toString(), // Convert integer to string for JWT
1580
+ tokenFamily: payload.tokenFamily,
1581
+ });
1582
+ // Update session with new token hashes (token rotation)
1583
+ // This automatically invalidates the old tokens as they won't match the session
1584
+ await this.sessionService.updateTokens(lockedSession.id, this.jwtService.hashToken(newTokens.accessToken), this.jwtService.hashToken(newTokens.refreshToken));
1585
+ this.logger?.log?.(`Token refreshed successfully for user ${payload.sub}`);
1586
+ // Decode new tokens to get expiry times
1587
+ const accessTokenValidation = await this.jwtService.validateAccessToken(newTokens.accessToken);
1588
+ const refreshTokenValidation = await this.jwtService.validateRefreshToken(newTokens.refreshToken);
1589
+ return {
1590
+ accessToken: newTokens.accessToken,
1591
+ refreshToken: newTokens.refreshToken,
1592
+ accessTokenExpiresAt: accessTokenValidation.payload?.exp || 0,
1593
+ refreshTokenExpiresAt: refreshTokenValidation.payload?.exp || 0,
1594
+ };
2083
1595
  }
2084
- // NOW validate the refresh token (after lock is acquired and reuse check)
2085
- // This ensures only one request can validate at a time per session
2086
- const validation = await this.jwtService.validateRefreshToken(refreshToken);
2087
- if (!validation.valid || !validation.payload) {
2088
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Invalid refresh token');
2089
- }
2090
- const payload = validation.payload;
2091
- // Re-check session after acquiring lock (it might have been revoked/updated)
2092
- // Since we have the lock, no other request can modify this session, but it might have been revoked
2093
- // We already have currentSession from the early reuse check, but re-fetch to ensure it's still valid
2094
- const lockedSession = (await this.sessionService.findByIdLight(session.id));
2095
- if (!lockedSession || lockedSession.isRevoked || lockedSession.id !== session.id) {
2096
- this.logger?.debug?.(`Session changed after lock acquisition for user ${payload.sub}. Session may have been revoked.`);
2097
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
1596
+ catch (error) {
1597
+ // Best-effort cookie cleanup for session-invalid refresh errors.
1598
+ if (error instanceof nauth_exception_1.NAuthException) {
1599
+ this.clearAuthCookiesOnRefreshFailure(error.code);
1600
+ }
1601
+ throw error;
2098
1602
  }
2099
- // ============================================================================
2100
- // NOTE: We still do the atomic mark operation below as a double-check
2101
- // The early check above handles cookie race conditions where old tokens
2102
- // are sent before new cookies are received
2103
- // ============================================================================
2104
- // Mark token as used BEFORE generating new tokens (prevents reuse)
2105
- if (this.config.jwt.refreshToken.reuseDetection) {
2106
- const refreshTokenTTL = this.jwtService.getRefreshTokenTTL();
2107
- const marked = await this.sessionService.markRefreshTokenAsUsed(tokenHash, refreshTokenTTL);
2108
- if (!marked) {
2109
- // Token was already marked as used - reuse detected!
2110
- this.logger?.error?.(`Token reuse detected for user ${payload.sub} - atomic mark failed, revoking entire token family ${payload.tokenFamily}`);
2111
- // Audit the reuse attempt
2112
- try {
2113
- const userForAudit = (await this.userRepository.findOne({
2114
- where: { sub: payload.sub },
2115
- }));
2116
- if (userForAudit) {
2117
- await this.auditService?.recordEvent({
2118
- userId: userForAudit.id,
2119
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.SUSPICIOUS_ACTIVITY,
2120
- eventStatus: 'SUSPICIOUS',
2121
- riskFactor: 75,
2122
- riskFactors: [risk_factor_enum_1.RiskFactor.TOKEN_REUSE_ATTEMPT],
2123
- reason: 'Token reuse attempt blocked',
2124
- // Client info automatically included from context
2125
- description: 'Refresh token reuse attempt detected via atomic operation. Legitimate user session preserved.',
2126
- metadata: {
2127
- tokenFamily: payload.tokenFamily,
2128
- detectedAt: new Date().toISOString(),
2129
- action: 'reuse_blocked_atomic',
2130
- },
2131
- });
2132
- }
2133
- }
2134
- catch (auditError) {
2135
- this.logger?.warn?.('Failed to record SUSPICIOUS_ACTIVITY audit event', { error: auditError });
2136
- }
2137
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Refresh token has already been used');
1603
+ finally {
1604
+ // Always release lock, even if error occurs
1605
+ // Only release if we successfully acquired it
1606
+ if (lockAcquired) {
1607
+ await this.sessionService.releaseRefreshLock(lockKey);
1608
+ this.logger?.debug?.(`[REFRESH DEBUG] Released lock ${lockKey}`);
2138
1609
  }
2139
- this.logger?.debug?.(`Marked refresh token as used for session ${lockedSession.id}`);
2140
1610
  }
2141
- // Generate new token pair with same family
2142
- // Note: deviceId not included in token - session.deviceId is source of truth
2143
- const newTokens = await this.jwtService.generateTokenPair({
2144
- userId: payload.sub,
2145
- email: payload.email,
2146
- sessionId: lockedSession.id.toString(), // Convert integer to string for JWT
2147
- tokenFamily: payload.tokenFamily,
2148
- });
2149
- // Update session with new token hashes (token rotation)
2150
- // This automatically invalidates the old tokens as they won't match the session
2151
- await this.sessionService.updateTokens(lockedSession.id, this.jwtService.hashToken(newTokens.accessToken), this.jwtService.hashToken(newTokens.refreshToken));
2152
- this.logger?.log?.(`Token refreshed successfully for user ${payload.sub}`);
2153
- // Decode new tokens to get expiry times
2154
- const accessTokenValidation = await this.jwtService.validateAccessToken(newTokens.accessToken);
2155
- const refreshTokenValidation = await this.jwtService.validateRefreshToken(newTokens.refreshToken);
2156
- return {
2157
- accessToken: newTokens.accessToken,
2158
- refreshToken: newTokens.refreshToken,
2159
- accessTokenExpiresAt: accessTokenValidation.payload?.exp || 0,
2160
- refreshTokenExpiresAt: refreshTokenValidation.payload?.exp || 0,
2161
- };
2162
1611
  }
2163
1612
  catch (error) {
2164
- // Best-effort cookie cleanup for session-invalid refresh errors.
1613
+ // Catch any errors that occur before the lock acquisition try block
1614
+ // Convert non-NAuthException errors to proper NAuthException to prevent 500 errors
2165
1615
  if (error instanceof nauth_exception_1.NAuthException) {
2166
1616
  this.clearAuthCookiesOnRefreshFailure(error.code);
1617
+ throw error;
2167
1618
  }
2168
- throw error;
2169
- }
2170
- finally {
2171
- // Always release lock, even if error occurs
2172
- // Only release if we successfully acquired it
2173
- if (lockAcquired) {
2174
- await this.sessionService.releaseRefreshLock(lockKey);
2175
- this.logger?.debug?.(`[REFRESH DEBUG] Released lock ${lockKey}`);
2176
- }
1619
+ // Log unexpected errors and convert to NAuthException
1620
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1621
+ this.logger?.error?.(`Unexpected error in refreshToken: ${errorMessage}`, {
1622
+ error,
1623
+ });
1624
+ this.clearAuthCookiesOnRefreshFailure(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID);
1625
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'An error occurred while refreshing the token');
2177
1626
  }
2178
1627
  }
2179
1628
  // ============================================================================
@@ -2199,16 +1648,25 @@ class AuthService {
2199
1648
  code !== error_codes_enum_1.AuthErrorCode.SESSION_EXPIRED) {
2200
1649
  return;
2201
1650
  }
2202
- const responseFromContext = this.clientInfoService.getResponse();
2203
- if (!responseFromContext)
2204
- return;
2205
- const response = responseFromContext;
2206
- if (typeof response.clearCookie === 'function') {
2207
- this.helpers.clearAuthCookies(response, false);
2208
- return;
1651
+ try {
1652
+ const responseFromContext = this.clientInfoService.getResponse();
1653
+ if (!responseFromContext)
1654
+ return;
1655
+ const response = responseFromContext;
1656
+ if (typeof response.clearCookie === 'function') {
1657
+ this.helpers.clearAuthCookies(response, false);
1658
+ return;
1659
+ }
1660
+ if (typeof response.cookie === 'function' || typeof response.setCookie === 'function') {
1661
+ (0, cookies_util_1.clearAuthCookies)(response, this.config, this.config.tokenDelivery?.cookieOptions, false);
1662
+ }
2209
1663
  }
2210
- if (typeof response.cookie === 'function' || typeof response.setCookie === 'function') {
2211
- (0, cookies_util_1.clearAuthCookies)(response, this.config, this.config.tokenDelivery?.cookieOptions, false);
1664
+ catch (error) {
1665
+ // Best-effort cookie clearing - don't let failures prevent proper error handling
1666
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1667
+ this.logger?.warn?.(`Failed to clear auth cookies on refresh failure: ${errorMessage}`, {
1668
+ error,
1669
+ });
2212
1670
  }
2213
1671
  }
2214
1672
  // ============================================================================
@@ -2231,9 +1689,8 @@ class AuthService {
2231
1689
  * - Requires authentication - session ID must be present in request context
2232
1690
  * - Endpoint MUST be protected by authentication guards
2233
1691
  * - User cannot specify which session to logout (always current session)
2234
- * - Optional sub validation for additional security
2235
1692
  *
2236
- * @param dto - Logout options (optional sub for validation, optional forgetMe flag)
1693
+ * @param dto - Logout options (optional forgetMe flag)
2237
1694
  * @returns Success status
2238
1695
  * @throws {NAuthException} SESSION_NOT_FOUND if session ID not found in request context
2239
1696
  *
@@ -2242,10 +1699,7 @@ class AuthService {
2242
1699
  * @UseGuards(AuthGuard)
2243
1700
  * @Get('logout')
2244
1701
  * async logout(@CurrentUser() user: IUser, @Query('forgetMe') forgetMe?: string) {
2245
- * const dto = new LogoutDTO();
2246
- * dto.sub = user.sub; // Optional validation
2247
- * dto.forgetMe = forgetMe === 'true';
2248
- * return this.authService.logout(dto);
1702
+ * return this.authService.logout({ forgetMe: forgetMe === 'true' });
2249
1703
  * }
2250
1704
  * ```
2251
1705
  */
@@ -2368,16 +1822,13 @@ class AuthService {
2368
1822
  * Optionally revokes all trusted devices if forgetDevices flag is set.
2369
1823
  *
2370
1824
  * Usage Patterns:
2371
- * - **User-initiated**: User logs out from all their own sessions (protected endpoint, user provides their own sub)
2372
- * - **Admin-initiated**: Admin force-logs out any user (admin-protected endpoint, admin provides target user's sub)
1825
+ * - **User-initiated**: User logs out from all their own sessions (protected endpoint)
2373
1826
  *
2374
1827
  * Security:
2375
- * - Requires explicit sub parameter
2376
- * - NO built-in authentication - endpoint MUST be protected by guards
2377
- * - For user endpoints: Extract sub from authenticated user context (@CurrentUser)
2378
- * - For admin endpoints: Accept sub from route parameter and protect with admin guards
1828
+ * - Uses authenticated user context for sub
1829
+ * - Endpoint MUST be protected by authentication guards
2379
1830
  *
2380
- * @param dto - User sub and optional forgetDevices flag
1831
+ * @param dto - Logout options (forgetDevices flag)
2381
1832
  * @returns Number of sessions revoked
2382
1833
  * @throws {NAuthException} NOT_FOUND if user not found
2383
1834
  *
@@ -2387,25 +1838,27 @@ class AuthService {
2387
1838
  * @UseGuards(AuthGuard)
2388
1839
  * @Post('logout/all')
2389
1840
  * async logoutAll(@CurrentUser() user: IUser, @Body() body: { forgetDevices?: boolean }) {
2390
- * return this.authService.logoutAll({ sub: user.sub, forgetDevices: body.forgetDevices });
1841
+ * return this.authService.logoutAll({ forgetDevices: body.forgetDevices });
2391
1842
  * }
2392
1843
  * ```
2393
1844
  *
2394
1845
  * @example Admin-initiated (admin manages any user)
2395
1846
  * ```typescript
2396
- * // Admin provides target user's sub
1847
+ * // Use AdminAuthService.logoutAll with target sub
2397
1848
  * @UseGuards(AuthGuard, AdminGuard)
2398
1849
  * @Post('admin/users/:sub/logout-all')
2399
1850
  * async adminLogoutAll(@Param('sub') sub: string, @Body() body: { forgetDevices?: boolean }) {
2400
- * return this.authService.logoutAll({ sub, forgetDevices: body.forgetDevices });
1851
+ * return this.adminAuthService.logoutAll({ sub, forgetDevices: body.forgetDevices });
2401
1852
  * }
2402
1853
  * ```
2403
1854
  */
2404
1855
  async logoutAll(dto) {
2405
1856
  // Ensure DTO is validated (supports direct usage without framework validation)
2406
1857
  dto = await (0, dto_validator_1.ensureValidatedDto)(logout_all_dto_1.LogoutAllDTO, dto);
1858
+ // Get current authenticated user from context
1859
+ const currentUser = this.getCurrentUserOrThrow();
2407
1860
  // Get user by sub to get internal id
2408
- const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
1861
+ const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
2409
1862
  if (!user) {
2410
1863
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2411
1864
  }
@@ -2434,6 +1887,7 @@ class AuthService {
2434
1887
  description: `Global signout: All trusted devices revoked (${revokedDevicesCount} device(s))`,
2435
1888
  metadata: {
2436
1889
  reason: 'global_logout_forget_devices',
1890
+ initiatedBy: 'user',
2437
1891
  revokedDevicesCount,
2438
1892
  devices: revokedDevices.map((d) => ({
2439
1893
  id: d.id,
@@ -2542,16 +1996,12 @@ class AuthService {
2542
1996
  * Current session (if called from authenticated context) is marked with isCurrent=true.
2543
1997
  *
2544
1998
  * Usage Patterns:
2545
- * - **User viewing own sessions**: User views their active sessions (protected endpoint, user provides their own sub)
2546
- * - **Admin viewing any user's sessions**: Admin views any user's sessions (admin-protected endpoint, admin provides target user's sub)
1999
+ * - **User viewing own sessions**: User views their active sessions (protected endpoint)
2547
2000
  *
2548
2001
  * Security:
2549
- * - Requires explicit sub parameter
2550
- * - NO built-in authentication - endpoint MUST be protected by guards
2551
- * - For user endpoints: Extract sub from authenticated user context (@CurrentUser)
2552
- * - For admin endpoints: Accept sub from route parameter and protect with admin guards
2002
+ * - Uses authenticated user context for sub
2003
+ * - Endpoint MUST be protected by authentication guards
2553
2004
  *
2554
- * @param dto - Contains user sub
2555
2005
  * @returns Array of sessions with device info, auth method, and isCurrent flag
2556
2006
  * @throws {NAuthException} NOT_FOUND if user not found
2557
2007
  *
@@ -2560,7 +2010,7 @@ class AuthService {
2560
2010
  * @UseGuards(AuthGuard)
2561
2011
  * @Get('sessions')
2562
2012
  * async getSessions(@CurrentUser() user: IUser) {
2563
- * return this.authService.getUserSessions({ sub: user.sub });
2013
+ * return this.authService.getUserSessions();
2564
2014
  * }
2565
2015
  * ```
2566
2016
  *
@@ -2569,15 +2019,15 @@ class AuthService {
2569
2019
  * @UseGuards(AuthGuard, AdminGuard)
2570
2020
  * @Get('admin/users/:sub/sessions')
2571
2021
  * async adminGetSessions(@Param('sub') sub: string) {
2572
- * return this.authService.getUserSessions({ sub });
2022
+ * return this.adminAuthService.getUserSessions({ sub });
2573
2023
  * }
2574
2024
  * ```
2575
2025
  */
2576
- async getUserSessions(dto) {
2577
- // Ensure DTO is validated (supports direct usage without framework validation)
2578
- dto = await (0, dto_validator_1.ensureValidatedDto)(get_user_sessions_dto_1.GetUserSessionsDTO, dto);
2026
+ async getUserSessions() {
2027
+ // Get current authenticated user from context
2028
+ const currentUser = this.getCurrentUserOrThrow();
2579
2029
  // Get user by sub to get internal id
2580
- const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
2030
+ const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
2581
2031
  if (!user) {
2582
2032
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2583
2033
  }
@@ -2625,6 +2075,47 @@ class AuthService {
2625
2075
  });
2626
2076
  return { sessions: sessionInfos };
2627
2077
  }
2078
+ /**
2079
+ * Get authentication audit history for current authenticated user
2080
+ *
2081
+ * Returns paginated audit trail of authentication events for the user:
2082
+ * - Login attempts (success/failure)
2083
+ * - Password changes
2084
+ * - MFA setup/verification
2085
+ * - Device trust events
2086
+ * - Device information, location, risk factors
2087
+ *
2088
+ * Usage Patterns:
2089
+ * - **User viewing own audit history**: User views their authentication history (protected endpoint)
2090
+ *
2091
+ * Security:
2092
+ * - Uses authenticated user context for sub
2093
+ * - Endpoint MUST be protected by authentication guards
2094
+ *
2095
+ * @param dto - Optional query parameters for filtering and pagination
2096
+ * @returns Paginated audit history response
2097
+ * @throws {NAuthException} FORBIDDEN if user not authenticated
2098
+ * @throws {NAuthException} NOT_FOUND if user not found
2099
+ *
2100
+ * @example User viewing own audit history
2101
+ * ```typescript
2102
+ * @UseGuards(AuthGuard)
2103
+ * @Get('audit/history')
2104
+ * async getAuditHistory(@Query() query: GetUserAuthHistoryDTO) {
2105
+ * return this.authService.getUserAuthHistory(query);
2106
+ * }
2107
+ * ```
2108
+ */
2109
+ async getUserAuthHistory(dto = {}) {
2110
+ if (!this.auditService) {
2111
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'Audit service is not available');
2112
+ }
2113
+ // Get current authenticated user from context
2114
+ const currentUser = this.getCurrentUserOrThrow();
2115
+ // Convert to admin DTO with sub from context
2116
+ const adminDto = Object.assign(new admin_get_user_auth_history_dto_1.AdminGetUserAuthHistoryDTO(), { ...dto, sub: currentUser.sub });
2117
+ return await this.auditService.getUserAuthHistory(adminDto);
2118
+ }
2628
2119
  /**
2629
2120
  * Logout a specific session by ID
2630
2121
  *
@@ -2633,17 +2124,14 @@ class AuthService {
2633
2124
  * Useful for "sign out from device" functionality in user dashboards.
2634
2125
  *
2635
2126
  * Usage Patterns:
2636
- * - **User logging out own session**: User revokes specific session (protected endpoint, user provides their own sub)
2637
- * - **Admin revoking any user's session**: Admin revokes specific session for any user (admin-protected endpoint, admin provides target user's sub)
2127
+ * - **User logging out own session**: User revokes specific session (protected endpoint)
2638
2128
  *
2639
2129
  * Security:
2640
- * - Requires explicit sub parameter
2130
+ * - Uses authenticated user context for sub
2641
2131
  * - Validates session belongs to user (prevents unauthorized session revocation)
2642
- * - NO built-in authentication - endpoint MUST be protected by guards
2643
- * - For user endpoints: Extract sub from authenticated user context (@CurrentUser)
2644
- * - For admin endpoints: Accept sub from route parameter and protect with admin guards
2132
+ * - Endpoint MUST be protected by authentication guards
2645
2133
  *
2646
- * @param dto - Contains sessionId and user sub
2134
+ * @param dto - Contains sessionId
2647
2135
  * @returns Success status and whether it was the current session
2648
2136
  * @throws {NAuthException} NOT_FOUND if user not found
2649
2137
  * @throws {NAuthException} SESSION_NOT_FOUND if session not found
@@ -2654,7 +2142,7 @@ class AuthService {
2654
2142
  * @UseGuards(AuthGuard)
2655
2143
  * @Delete('sessions/:sessionId')
2656
2144
  * async logoutSession(@CurrentUser() user: IUser, @Param('sessionId') sessionId: string) {
2657
- * return this.authService.logoutSession({ sub: user.sub, sessionId });
2145
+ * return this.authService.logoutSession({ sessionId });
2658
2146
  * }
2659
2147
  * ```
2660
2148
  *
@@ -2663,15 +2151,17 @@ class AuthService {
2663
2151
  * @UseGuards(AuthGuard, AdminGuard)
2664
2152
  * @Delete('admin/users/:sub/sessions/:sessionId')
2665
2153
  * async adminRevokeSession(@Param('sub') sub: string, @Param('sessionId') sessionId: string) {
2666
- * return this.authService.logoutSession({ sub, sessionId });
2154
+ * return this.adminAuthService.revokeUserSession({ sub, sessionId });
2667
2155
  * }
2668
2156
  * ```
2669
2157
  */
2670
2158
  async logoutSession(dto) {
2671
2159
  // Ensure DTO is validated (supports direct usage without framework validation)
2672
2160
  dto = await (0, dto_validator_1.ensureValidatedDto)(logout_session_dto_1.LogoutSessionDTO, dto);
2161
+ // Get current authenticated user from context
2162
+ const currentUser = this.getCurrentUserOrThrow();
2673
2163
  // Get user by sub to get internal id
2674
- const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
2164
+ const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
2675
2165
  if (!user) {
2676
2166
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2677
2167
  }
@@ -2695,7 +2185,7 @@ class AuthService {
2695
2185
  const wasCurrentSession = currentSessionId !== null && sessionId === currentSessionId;
2696
2186
  // Revoke the session
2697
2187
  await this.sessionService.revokeSession(sessionId, 'User requested logout', {
2698
- requestedBy: dto.sub,
2188
+ requestedBy: currentUser.sub,
2699
2189
  wasCurrentSession,
2700
2190
  });
2701
2191
  // Clear cookies if this was the current session
@@ -2745,14 +2235,13 @@ class AuthService {
2745
2235
  * checks password reuse policy, and updates the user's password hash and history.
2746
2236
  * Executes configured pre-change hooks if provided.
2747
2237
  *
2748
- * @param sub - External user identifier (sub/UUID)
2749
2238
  * @param dto - ChangePasswordDTO containing old and new password
2750
2239
  * @returns void
2751
2240
  * @throws {NAuthException} If the user is not found, current password is incorrect, the new password is weak, password reuse is detected, or password change is disallowed by hooks.
2752
2241
  *
2753
2242
  * @example
2754
2243
  * ```typescript
2755
- * await authService.changePassword('user-uuid', {
2244
+ * await authService.changePassword({
2756
2245
  * oldPassword: 'currentPass123!',
2757
2246
  * newPassword: 'newStr0ngPass!@#',
2758
2247
  * });
@@ -2760,30 +2249,42 @@ class AuthService {
2760
2249
  */
2761
2250
  async changePassword(dto) {
2762
2251
  // Ensure DTO is validated (supports direct usage without framework validation)
2763
- dto = await (0, dto_validator_1.ensureValidatedDto)(change_password_request_dto_1.ChangePasswordRequestDTO, dto);
2252
+ dto = await (0, dto_validator_1.ensureValidatedDto)(change_password_dto_1.ChangePasswordDTO, dto);
2253
+ // Get current authenticated user from context
2254
+ const currentUser = this.getCurrentUserOrThrow();
2764
2255
  // Get user by sub
2765
- const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
2766
- if (!user || !user.passwordHash) {
2256
+ const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
2257
+ if (!user) {
2767
2258
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2768
2259
  }
2769
2260
  // ============================================================================
2261
+ // Social-only accounts: allow setting first password without old password
2262
+ // ============================================================================
2263
+ if (!user.passwordHash) {
2264
+ if (dto.oldPassword !== '') {
2265
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password change not allowed');
2266
+ }
2267
+ }
2268
+ // ============================================================================
2770
2269
  // Lifecycle Hook: beforePasswordChange (TODO: Implement provider-based hook)
2771
2270
  // ============================================================================
2772
2271
  // TODO: Implement provider-based hook for beforePasswordChange
2773
- // const allowed = await this.hookRegistry.executeBeforePasswordChange(dto.sub, dto.oldPassword);
2272
+ // const allowed = await this.hookRegistry.executeBeforePasswordChange(currentUser.sub, dto.oldPassword);
2774
2273
  // if (!allowed) {
2775
2274
  // throw new NAuthException(AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password change not allowed');
2776
2275
  // }
2777
- // Verify old password
2778
- const isValid = await this.passwordService.verifyPassword(dto.oldPassword, user.passwordHash);
2779
- if (!isValid) {
2780
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_INCORRECT, 'Current password is incorrect');
2276
+ if (user.passwordHash) {
2277
+ // Verify old password
2278
+ const isValid = await this.passwordService.verifyPassword(dto.oldPassword, user.passwordHash);
2279
+ if (!isValid) {
2280
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_INCORRECT, 'Current password is incorrect');
2281
+ }
2781
2282
  }
2782
2283
  // ============================================================================
2783
2284
  // Lifecycle Hook: afterPasswordChange (TODO: Implement provider-based hook)
2784
2285
  // ============================================================================
2785
2286
  // TODO: Implement provider-based hook for afterPasswordChange
2786
- // await this.hookRegistry.executeAfterPasswordChange(dto.sub);
2287
+ // await this.hookRegistry.executeAfterPasswordChange(currentUser.sub);
2787
2288
  await this.helpers.updateUserPassword({
2788
2289
  user,
2789
2290
  newPassword: dto.newPassword,
@@ -2802,53 +2303,38 @@ class AuthService {
2802
2303
  *
2803
2304
  * Updates user fields (name, email, phone, username, metadata) and enforces unique constraints and verification rules.
2804
2305
  *
2805
- * @param dto - UpdateUserAttributesRequestDTO containing sub and fields to update
2306
+ * @param dto - UpdateUserAttributesDTO containing fields to update
2806
2307
  * @returns Updated user object
2807
2308
  * @throws {NAuthException} If user not found or unique constraint violated
2808
2309
  *
2809
2310
  * @example
2810
- * await authService.updateUserAttributes({ sub: 'user-uuid', email: 'test@example.com' });
2311
+ * await authService.updateUserAttributes({ email: 'test@example.com' });
2811
2312
  */
2812
2313
  async updateUserAttributes(dto) {
2813
- return await this.userService.updateUserAttributes(dto);
2314
+ const currentUser = this.getCurrentUserOrThrow();
2315
+ const adminDto = Object.assign(new admin_update_user_attributes_dto_1.AdminUpdateUserAttributesDTO(), { sub: currentUser.sub, ...dto });
2316
+ return await this.userService.updateUserAttributes(adminDto);
2814
2317
  }
2815
2318
  /**
2816
- * Update email and/or phone verification status.
2817
- *
2818
- * Intended for admin use cases such as migration or offline validation.
2819
- * Updates verification status without requiring actual verification codes.
2319
+ * Get user for authentication context
2820
2320
  *
2821
- * Validation:
2822
- * - Cannot set verified=true if email/phone doesn't exist
2823
- * - Can set verified=false even if email/phone doesn't exist (default state)
2824
- * - Only updates provided fields (partial update)
2321
+ * Loads user by sub (external identifier) with all fields needed for auth context.
2322
+ * Computes hasPasswordHash from passwordHash, then removes passwordHash and other sensitive fields.
2825
2323
  *
2826
- * Audit:
2827
- * - Records EMAIL_VERIFIED or PHONE_VERIFIED audit events
2828
- * - Includes performedBy from authenticated admin context
2324
+ * This method is used by AuthHandler and AuthGuard to load authenticated users.
2325
+ * It ensures consistent user object shape across platforms (core + NestJS).
2829
2326
  *
2830
- * @param dto - Request DTO containing sub and verification status flags
2831
- * @returns Updated user object
2832
- * @throws {NAuthException} If user not found or trying to verify non-existent email/phone
2327
+ * @param sub - External user identifier (UUID)
2328
+ * @returns User object with hasPasswordHash flag, without sensitive fields
2329
+ * @throws {NAuthException} If user not found or account is inactive
2833
2330
  *
2834
2331
  * @example
2835
2332
  * ```typescript
2836
- * // Update email verification only
2837
- * await authService.updateVerifiedStatus({
2838
- * sub: 'user-uuid',
2839
- * isEmailVerified: true
2840
- * });
2841
- *
2842
- * // Update both email and phone verification
2843
- * await authService.updateVerifiedStatus({
2844
- * sub: 'user-uuid',
2845
- * isEmailVerified: true,
2846
- * isPhoneVerified: false
2847
- * });
2333
+ * const user = await authService.getUserForAuthContext('user-uuid');
2848
2334
  * ```
2849
2335
  */
2850
- async updateVerifiedStatus(dto) {
2851
- return await this.userService.updateVerifiedStatus(dto);
2336
+ async getUserForAuthContext(sub) {
2337
+ return await this.userService.getUserForAuthContext(sub);
2852
2338
  }
2853
2339
  /**
2854
2340
  * Validate JWT access token
@@ -2886,7 +2372,6 @@ class AuthService {
2886
2372
  * ```
2887
2373
  */
2888
2374
  async validateAccessToken(dto) {
2889
- // Ensure DTO is validated (supports direct usage without framework validation)
2890
2375
  dto = await (0, dto_validator_1.ensureValidatedDto)(validate_access_token_dto_1.ValidateAccessTokenDTO, dto);
2891
2376
  const result = await this.jwtService.validateAccessToken(dto.accessToken);
2892
2377
  return {
@@ -2897,368 +2382,6 @@ class AuthService {
2897
2382
  };
2898
2383
  }
2899
2384
  // ============================================================================
2900
- // Helper Methods
2901
- // ============================================================================
2902
- // NOTE: Private helper methods have been moved to AuthServiceInternalHelpers
2903
- // Use this.helpers.methodName() to access them
2904
- /**
2905
- * Get user for authentication context
2906
- *
2907
- * Loads user by sub (external identifier) with all fields needed for auth context.
2908
- * Computes hasPasswordHash from passwordHash, then removes passwordHash and other sensitive fields.
2909
- *
2910
- * This method is used by AuthHandler and AuthGuard to load authenticated users.
2911
- * It ensures consistent user object shape across platforms (core + NestJS).
2912
- *
2913
- * @param sub - External user identifier (UUID)
2914
- * @returns User object with hasPasswordHash flag, without sensitive fields
2915
- * @throws {NAuthException} If user not found or account is inactive
2916
- *
2917
- * @example
2918
- * ```typescript
2919
- * const user = await authService.getUserForAuthContext('user-uuid-123');
2920
- * // user.hasPasswordHash === true/false
2921
- * // user.passwordHash === undefined (removed)
2922
- * ```
2923
- */
2924
- async getUserForAuthContext(sub) {
2925
- return await this.userService.getUserForAuthContext(sub);
2926
- }
2927
- /**
2928
- * Get user by external identifier (sub/UUID).
2929
- *
2930
- * @param dto - GetUserByIdDTO containing sub
2931
- * @returns User response DTO or null if not found
2932
- *
2933
- * @example
2934
- * ```typescript
2935
- * const user = await authService.getUserById({ sub: 'user-uuid' });
2936
- * ```
2937
- */
2938
- async getUserById(dto) {
2939
- return await this.userService.getUserById(dto);
2940
- }
2941
- /**
2942
- * Get user by email address.
2943
- *
2944
- * @param dto - GetUserByEmailDTO containing email and optional requireEmailVerified
2945
- * @returns User response DTO or null if not found
2946
- * @internal - For use by social auth providers
2947
- *
2948
- * @example
2949
- * ```typescript
2950
- * const user = await authService.getUserByEmail({ email: 'user@example.com', requireEmailVerified: true });
2951
- * ```
2952
- */
2953
- async getUserByEmail(dto) {
2954
- return await this.userService.getUserByEmail(dto);
2955
- }
2956
- /**
2957
- * Require user to change password at next login.
2958
- *
2959
- * Throws if user not found or has no password set (e.g. social login only).
2960
- *
2961
- * @param dto - SetMustChangePasswordDTO containing userId (sub)
2962
- * @returns Success response
2963
- * @throws {NAuthException} If user is not found or cannot change password
2964
- *
2965
- * @example
2966
- * ```typescript
2967
- * await authService.setMustChangePassword({ userId: 'user-uuid-123' });
2968
- * ```
2969
- */
2970
- async setMustChangePassword(dto) {
2971
- return await this.userService.setMustChangePassword(dto);
2972
- }
2973
- /**
2974
- * Admin-only: Initiate a code-based password reset workflow.
2975
- *
2976
- * Unlike adminSetPassword(), this sends a verification code (and optional link)
2977
- * to the user via email/SMS and allows them to set their own password.
2978
- *
2979
- * Features:
2980
- * - Code + optional link delivery (like email verification)
2981
- * - Optional immediate session revocation
2982
- * - Configurable expiry (default 1 hour)
2983
- * - Admin-specific email template
2984
- * - No rate limiting (admin bypass)
2985
- * - Separate audit trail with reason
2986
- *
2987
- * Security:
2988
- * - Admin-only operation (protect route with admin guard)
2989
- * - Non-enumerating (throws NOT_FOUND if user doesn't exist)
2990
- * - Separate token type ('admin_password_reset')
2991
- * - Audit logging with reason
2992
- *
2993
- * @param dto - Admin reset password request
2994
- * @returns Response with masked destination, expiry, and sessions revoked count
2995
- * @throws {NAuthException} NOT_FOUND when user not found
2996
- *
2997
- * @example
2998
- * ```typescript
2999
- * // With link for custom UI
3000
- * const result = await authService.adminResetPassword({
3001
- * identifier: 'user@example.com',
3002
- * baseUrl: 'https://myapp.com/reset-password',
3003
- * revokeSessions: true,
3004
- * reason: 'User reported compromise'
3005
- * });
3006
- * // result: { success: true, destination: 'u***r@example.com', expiresIn: 3600, sessionsRevoked: 3 }
3007
- *
3008
- * // Code only (no link)
3009
- * const result = await authService.adminResetPassword({
3010
- * identifier: 'user@example.com'
3011
- * });
3012
- * ```
3013
- */
3014
- async adminResetPassword(dto) {
3015
- // Ensure DTO is validated (supports direct usage without framework validation)
3016
- dto = await (0, dto_validator_1.ensureValidatedDto)(admin_reset_password_dto_1.AdminResetPasswordDTO, dto);
3017
- this.logger?.log?.(`Admin password reset requested for identifier: ${dto.identifier}`);
3018
- this.logger?.debug?.(`Reset details: { identifier: ${dto.identifier}, deliveryMethod: ${dto.deliveryMethod ?? 'email'}, revokeSessions: ${dto.revokeSessions ?? false}, baseUrl: ${dto.baseUrl ?? 'none'}, reason: ${dto.reason ?? 'none'} }`);
3019
- // ============================================================================
3020
- // Find User by Identifier
3021
- // ============================================================================
3022
- // Support multiple identifier types: email, username, phone, or sub (UUID)
3023
- let user = null;
3024
- // Try to find by sub (UUID) first if it looks like a UUID.
3025
- // WHY: Many deployments treat `sub` as the primary immutable identifier.
3026
- if ((0, class_validator_1.isUUID)(dto.identifier)) {
3027
- this.logger?.debug?.(`Identifier appears to be UUID, searching by sub: ${dto.identifier}`);
3028
- user = (await this.userRepository.findOne({ where: { sub: dto.identifier } }));
3029
- }
3030
- // If not found by sub, try by identifier (email, username, phone)
3031
- if (!user) {
3032
- this.logger?.debug?.(`Searching by identifier (email/username/phone): ${dto.identifier}`);
3033
- user = await this.helpers.findUserByIdentifier(dto.identifier);
3034
- }
3035
- if (!user) {
3036
- this.logger?.warn?.(`Admin password reset failed - user not found: ${dto.identifier}`);
3037
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
3038
- }
3039
- if (!this.passwordResetService) {
3040
- this.logger?.error?.('Password reset service not available');
3041
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SERVICE_UNAVAILABLE, 'Password reset service is not configured. Please configure an email provider.');
3042
- }
3043
- // ============================================================================
3044
- // Optionally revoke sessions immediately (before sending reset email)
3045
- // ============================================================================
3046
- const revokeSessions = dto.revokeSessions ?? false;
3047
- let sessionsRevoked = 0;
3048
- if (revokeSessions) {
3049
- sessionsRevoked = await this.sessionService.revokeAllUserSessions(user.id, 'Admin initiated password reset');
3050
- this.logger?.log?.(`Revoked ${sessionsRevoked} sessions for user ${user.sub}`);
3051
- }
3052
- // ============================================================================
3053
- // Request admin reset with code + link
3054
- // ============================================================================
3055
- const delivery = dto.deliveryMethod || 'email';
3056
- const expiresIn = dto.codeExpiresIn || 3600; // Default 1 hour
3057
- const result = await this.passwordResetService.requestAdminReset(user, delivery, {
3058
- expiresIn,
3059
- baseUrl: dto.baseUrl, // Consumer app can build custom UI
3060
- });
3061
- // ============================================================================
3062
- // Audit Logging
3063
- // ============================================================================
3064
- await this.auditService?.recordEvent({
3065
- userId: user.id,
3066
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ADMIN_PASSWORD_RESET_INITIATED,
3067
- eventStatus: 'INFO',
3068
- authMethod: 'password',
3069
- description: dto.reason || 'Admin initiated password reset',
3070
- reason: dto.reason, // Store reason in audit event
3071
- metadata: {
3072
- medium: delivery,
3073
- expiresIn,
3074
- sessionsRevoked,
3075
- hasBaseUrl: !!dto.baseUrl,
3076
- },
3077
- });
3078
- // ============================================================================
3079
- // Return Response
3080
- // ============================================================================
3081
- return {
3082
- success: true,
3083
- destination: result.destination,
3084
- deliveryMedium: result.deliveryMedium,
3085
- expiresIn: result.expiresIn,
3086
- sessionsRevoked: revokeSessions ? sessionsRevoked : undefined,
3087
- };
3088
- }
3089
- /**
3090
- * Complete admin-initiated password reset with a verification code.
3091
- *
3092
- * NOTE:
3093
- * - Links (when provided) should include the same verification code as a query parameter
3094
- * (e.g., `...?code=123456`) to keep consumer apps code-only and consistent.
3095
- *
3096
- * Security:
3097
- * - Verifies code via PasswordResetService
3098
- * - Enforces password policy and history
3099
- * - Always revokes all sessions on completion
3100
- * - Does not force password change (user already set new password)
3101
- * - Records audit event
3102
- *
3103
- * @param dto - Confirm admin reset password request
3104
- * @returns Success response
3105
- * @throws {NAuthException} NOT_FOUND | PASSWORD_RESET_CODE_INVALID | PASSWORD_RESET_CODE_EXPIRED | PASSWORD_RESET_MAX_ATTEMPTS | WEAK_PASSWORD | PASSWORD_REUSED | INVALID_CREDENTIALS
3106
- *
3107
- * @example
3108
- * ```typescript
3109
- * await authService.confirmAdminResetPassword({
3110
- * identifier: 'user@example.com',
3111
- * code: '123456',
3112
- * newPassword: 'NewSecurePass123!'
3113
- * });
3114
- * ```
3115
- */
3116
- async confirmAdminResetPassword(dto) {
3117
- // Ensure DTO is validated (supports direct usage without framework validation)
3118
- dto = await (0, dto_validator_1.ensureValidatedDto)(admin_reset_password_dto_1.ConfirmAdminResetPasswordDTO, dto);
3119
- this.logger?.log?.(`Confirm admin password reset for identifier: ${dto.identifier}`);
3120
- // ============================================================================
3121
- // Find User by Identifier
3122
- // ============================================================================
3123
- let user = null;
3124
- if ((0, class_validator_1.isUUID)(dto.identifier)) {
3125
- this.logger?.debug?.(`Identifier appears to be UUID, searching by sub: ${dto.identifier}`);
3126
- user = (await this.userRepository.findOne({ where: { sub: dto.identifier } }));
3127
- }
3128
- if (!user) {
3129
- this.logger?.debug?.(`Searching by identifier (email/username/phone): ${dto.identifier}`);
3130
- user = await this.helpers.findUserByIdentifier(dto.identifier);
3131
- }
3132
- if (!user) {
3133
- this.logger?.warn?.(`Confirm admin reset failed - user not found: ${dto.identifier}`);
3134
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
3135
- }
3136
- if (!this.passwordResetService) {
3137
- this.logger?.error?.('Password reset service not available');
3138
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SERVICE_UNAVAILABLE, 'Password reset service is not configured. Please configure an email provider.');
3139
- }
3140
- // ============================================================================
3141
- // Verify code
3142
- // ============================================================================
3143
- await this.passwordResetService.consumeValidCode(user, dto.code, 'admin_password_reset');
3144
- // ============================================================================
3145
- // Update password
3146
- // ============================================================================
3147
- // WHY: User already set a new password via this reset flow, so no need to force
3148
- // another password change on next login (unlike adminSetPassword where admin sets
3149
- // a password the user doesn't know)
3150
- await this.helpers.updateUserPassword({
3151
- user,
3152
- newPassword: dto.newPassword,
3153
- mustChangePassword: false, // User already set new password, no need to force change again
3154
- revokeSessions: true, // Always revoke on completion
3155
- revokeReason: 'Admin-initiated password reset completed',
3156
- audit: {
3157
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ADMIN_PASSWORD_RESET_COMPLETED,
3158
- eventStatus: 'SUCCESS',
3159
- description: 'User completed admin-initiated password reset',
3160
- metadata: {
3161
- usedCode: true,
3162
- },
3163
- },
3164
- }, this.passwordService, this.auditService);
3165
- // ============================================================================
3166
- // Return Response
3167
- // ============================================================================
3168
- return {
3169
- success: true,
3170
- };
3171
- }
3172
- /**
3173
- * Admin-only: Reset a user's password by identifier.
3174
- *
3175
- * Allows administrators to reset a user's password using any identifier
3176
- * (email, username, phone, or sub). Automatically revokes sessions and optionally
3177
- * requires password change on next login using the existing challenge system.
3178
- *
3179
- * SECURITY: This is an admin-only operation. Ensure proper authorization
3180
- * checks are in place before calling this method.
3181
- *
3182
- * @param dto - Admin reset password request
3183
- * @returns Response with success status and session revocation count
3184
- * @throws {NAuthException} If user not found, user has no password (social-only), or password validation fails
3185
- *
3186
- * @example
3187
- * ```typescript
3188
- * // Reset with force password change
3189
- * const result = await authService.adminSetPassword({
3190
- * identifier: 'user@example.com',
3191
- * newPassword: 'NewSecurePassword123!',
3192
- * mustChangePassword: true,
3193
- * revokeSessions: true
3194
- * });
3195
- *
3196
- * // Reset without forcing password change
3197
- * const result = await authService.adminSetPassword({
3198
- * identifier: 'a21b654c-2746-4168-acee-c175083a65cd',
3199
- * newPassword: 'NewSecurePassword123!',
3200
- * mustChangePassword: false
3201
- * });
3202
- * ```
3203
- */
3204
- async adminSetPassword(dto) {
3205
- // Ensure DTO is validated (supports direct usage without framework validation)
3206
- dto = await (0, dto_validator_1.ensureValidatedDto)(admin_set_password_dto_1.AdminSetPasswordDTO, dto);
3207
- this.logger?.log?.(`Admin password reset requested for identifier: ${dto.identifier}`);
3208
- this.logger?.debug?.(`Reset details: { identifier: ${dto.identifier}, mustChangePassword: ${dto.mustChangePassword ?? true}, revokeSessions: ${dto.revokeSessions ?? true} }`);
3209
- // ============================================================================
3210
- // Find User by Identifier
3211
- // ============================================================================
3212
- // Support multiple identifier types: email, username, phone, or sub (UUID)
3213
- let user = null;
3214
- // Try to find by sub (UUID) first if it looks like a UUID.
3215
- // WHY: Many deployments treat `sub` as the primary immutable identifier.
3216
- if ((0, class_validator_1.isUUID)(dto.identifier)) {
3217
- this.logger?.debug?.(`Identifier appears to be UUID, searching by sub: ${dto.identifier}`);
3218
- user = (await this.userRepository.findOne({ where: { sub: dto.identifier } }));
3219
- }
3220
- // If not found by sub, try by identifier (email, username, phone)
3221
- if (!user) {
3222
- this.logger?.debug?.(`Searching by identifier (email/username/phone): ${dto.identifier}`);
3223
- user = await this.helpers.findUserByIdentifier(dto.identifier);
3224
- }
3225
- if (!user) {
3226
- this.logger?.warn?.(`Password reset failed - user not found: ${dto.identifier}`);
3227
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
3228
- }
3229
- const mustChangePassword = dto.mustChangePassword ?? true; // Default to true for security
3230
- const revokeSessions = dto.revokeSessions !== false;
3231
- const wasSocialOnly = !user.passwordHash;
3232
- const { sessionsRevoked } = await this.helpers.updateUserPassword({
3233
- user,
3234
- newPassword: dto.newPassword,
3235
- mustChangePassword,
3236
- revokeSessions,
3237
- revokeReason: 'Password reset by administrator',
3238
- audit: {
3239
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.PASSWORD_RESET_COMPLETED,
3240
- eventStatus: 'SUCCESS',
3241
- reason: 'admin_reset',
3242
- description: 'Password reset by administrator',
3243
- metadata: {
3244
- identifier: dto.identifier,
3245
- mustChangePassword,
3246
- // WHY: Admins can set the first password for social-only accounts so users can login via either route later.
3247
- // This flag helps downstream observability without exposing anything to clients.
3248
- wasSocialOnly,
3249
- },
3250
- },
3251
- }, this.passwordService, this.auditService);
3252
- // ============================================================================
3253
- // Return Response
3254
- // ============================================================================
3255
- return {
3256
- success: true,
3257
- mustChangePassword,
3258
- sessionsRevoked,
3259
- };
3260
- }
3261
- // ============================================================================
3262
2385
  // Forgot Password (Account Recovery)
3263
2386
  // ============================================================================
3264
2387
  /**
@@ -3394,6 +2517,16 @@ class AuthService {
3394
2517
  }, this.passwordService, this.auditService);
3395
2518
  return { success: true, mustChangePassword: false };
3396
2519
  }
2520
+ // ============================================================================
2521
+ // Helper Methods
2522
+ // ============================================================================
2523
+ getCurrentUserOrThrow() {
2524
+ const currentUser = context_storage_1.ContextStorage.get('CURRENT_USER');
2525
+ if (!currentUser) {
2526
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.FORBIDDEN, 'Authentication required');
2527
+ }
2528
+ return currentUser;
2529
+ }
3397
2530
  }
3398
2531
  exports.AuthService = AuthService;
3399
2532
  //# sourceMappingURL=auth.service.js.map