@nauth-toolkit/core 0.1.87 → 0.1.88

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 (163) hide show
  1. package/dist/dto/admin-get-user-auth-history.dto.d.ts +62 -0
  2. package/dist/dto/admin-get-user-auth-history.dto.d.ts.map +1 -0
  3. package/dist/dto/admin-get-user-auth-history.dto.js +87 -0
  4. package/dist/dto/admin-get-user-auth-history.dto.js.map +1 -0
  5. package/dist/dto/admin-logout-all.dto.d.ts +48 -0
  6. package/dist/dto/admin-logout-all.dto.d.ts.map +1 -0
  7. package/dist/dto/{change-password-request.dto.js → admin-logout-all.dto.js} +36 -21
  8. package/dist/dto/admin-logout-all.dto.js.map +1 -0
  9. package/dist/dto/admin-remove-devices.dto.d.ts +25 -0
  10. package/dist/dto/admin-remove-devices.dto.d.ts.map +1 -0
  11. package/dist/dto/admin-remove-devices.dto.js +50 -0
  12. package/dist/dto/admin-remove-devices.dto.js.map +1 -0
  13. package/dist/dto/admin-reset-password.dto.d.ts +15 -19
  14. package/dist/dto/admin-reset-password.dto.d.ts.map +1 -1
  15. package/dist/dto/admin-reset-password.dto.js +21 -41
  16. package/dist/dto/admin-reset-password.dto.js.map +1 -1
  17. package/dist/dto/admin-revoke-session.dto.d.ts +22 -0
  18. package/dist/dto/admin-revoke-session.dto.d.ts.map +1 -0
  19. package/dist/dto/admin-revoke-session.dto.js +48 -0
  20. package/dist/dto/admin-revoke-session.dto.js.map +1 -0
  21. package/dist/dto/admin-set-password.dto.d.ts +8 -10
  22. package/dist/dto/admin-set-password.dto.d.ts.map +1 -1
  23. package/dist/dto/admin-set-password.dto.js +11 -21
  24. package/dist/dto/admin-set-password.dto.js.map +1 -1
  25. package/dist/dto/admin-set-preferred-method.dto.d.ts +25 -0
  26. package/dist/dto/admin-set-preferred-method.dto.d.ts.map +1 -0
  27. package/dist/dto/admin-set-preferred-method.dto.js +50 -0
  28. package/dist/dto/admin-set-preferred-method.dto.js.map +1 -0
  29. package/dist/dto/admin-update-user-attributes.dto.d.ts +41 -0
  30. package/dist/dto/admin-update-user-attributes.dto.d.ts.map +1 -0
  31. package/dist/dto/{update-user-attributes-request.dto.js → admin-update-user-attributes.dto.js} +12 -17
  32. package/dist/dto/admin-update-user-attributes.dto.js.map +1 -0
  33. package/dist/dto/auth-challenge.dto.d.ts +2 -2
  34. package/dist/dto/auth-challenge.dto.d.ts.map +1 -1
  35. package/dist/dto/auth-challenge.dto.js +3 -3
  36. package/dist/dto/auth-challenge.dto.js.map +1 -1
  37. package/dist/dto/auth-response.dto.d.ts +1 -1
  38. package/dist/dto/auth-response.dto.d.ts.map +1 -1
  39. package/dist/dto/auth-response.dto.js +1 -1
  40. package/dist/dto/auth-response.dto.js.map +1 -1
  41. package/dist/dto/get-mfa-status.dto.d.ts +8 -4
  42. package/dist/dto/get-mfa-status.dto.d.ts.map +1 -1
  43. package/dist/dto/get-mfa-status.dto.js +8 -4
  44. package/dist/dto/get-mfa-status.dto.js.map +1 -1
  45. package/dist/dto/get-risk-assessment-history.dto.d.ts +3 -3
  46. package/dist/dto/get-risk-assessment-history.dto.d.ts.map +1 -1
  47. package/dist/dto/get-risk-assessment-history.dto.js +5 -5
  48. package/dist/dto/get-risk-assessment-history.dto.js.map +1 -1
  49. package/dist/dto/get-suspicious-activity.dto.d.ts +3 -3
  50. package/dist/dto/get-suspicious-activity.dto.d.ts.map +1 -1
  51. package/dist/dto/get-suspicious-activity.dto.js +5 -5
  52. package/dist/dto/get-suspicious-activity.dto.js.map +1 -1
  53. package/dist/dto/get-user-auth-history.dto.d.ts +4 -39
  54. package/dist/dto/get-user-auth-history.dto.d.ts.map +1 -1
  55. package/dist/dto/get-user-auth-history.dto.js +53 -51
  56. package/dist/dto/get-user-auth-history.dto.js.map +1 -1
  57. package/dist/dto/get-user-devices.dto.d.ts +5 -18
  58. package/dist/dto/get-user-devices.dto.d.ts.map +1 -1
  59. package/dist/dto/get-user-devices.dto.js +5 -39
  60. package/dist/dto/get-user-devices.dto.js.map +1 -1
  61. package/dist/dto/get-user-sessions-response.dto.d.ts +1 -1
  62. package/dist/dto/get-user-sessions-response.dto.js +1 -1
  63. package/dist/dto/get-user-sessions.dto.d.ts +1 -1
  64. package/dist/dto/get-user-sessions.dto.js +1 -1
  65. package/dist/dto/index.d.ts +8 -2
  66. package/dist/dto/index.d.ts.map +1 -1
  67. package/dist/dto/index.js +8 -2
  68. package/dist/dto/index.js.map +1 -1
  69. package/dist/dto/logout-all-response.dto.d.ts +1 -1
  70. package/dist/dto/logout-all-response.dto.js +1 -1
  71. package/dist/dto/logout-all.dto.d.ts +1 -18
  72. package/dist/dto/logout-all.dto.d.ts.map +1 -1
  73. package/dist/dto/logout-all.dto.js +1 -30
  74. package/dist/dto/logout-all.dto.js.map +1 -1
  75. package/dist/dto/logout-session.dto.d.ts +0 -5
  76. package/dist/dto/logout-session.dto.d.ts.map +1 -1
  77. package/dist/dto/logout-session.dto.js +0 -12
  78. package/dist/dto/logout-session.dto.js.map +1 -1
  79. package/dist/dto/logout.dto.d.ts +1 -18
  80. package/dist/dto/logout.dto.d.ts.map +1 -1
  81. package/dist/dto/logout.dto.js +1 -30
  82. package/dist/dto/logout.dto.js.map +1 -1
  83. package/dist/dto/remove-devices.dto.d.ts +4 -16
  84. package/dist/dto/remove-devices.dto.d.ts.map +1 -1
  85. package/dist/dto/remove-devices.dto.js +4 -26
  86. package/dist/dto/remove-devices.dto.js.map +1 -1
  87. package/dist/dto/set-mfa-exemption.dto.d.ts +8 -9
  88. package/dist/dto/set-mfa-exemption.dto.d.ts.map +1 -1
  89. package/dist/dto/set-mfa-exemption.dto.js +11 -13
  90. package/dist/dto/set-mfa-exemption.dto.js.map +1 -1
  91. package/dist/dto/set-must-change-password.dto.d.ts +3 -3
  92. package/dist/dto/set-must-change-password.dto.d.ts.map +1 -1
  93. package/dist/dto/set-must-change-password.dto.js +5 -5
  94. package/dist/dto/set-must-change-password.dto.js.map +1 -1
  95. package/dist/dto/set-preferred-method.dto.d.ts +4 -16
  96. package/dist/dto/set-preferred-method.dto.d.ts.map +1 -1
  97. package/dist/dto/set-preferred-method.dto.js +4 -26
  98. package/dist/dto/set-preferred-method.dto.js.map +1 -1
  99. package/dist/dto/setup-mfa.dto.d.ts +3 -18
  100. package/dist/dto/setup-mfa.dto.d.ts.map +1 -1
  101. package/dist/dto/setup-mfa.dto.js +3 -30
  102. package/dist/dto/setup-mfa.dto.js.map +1 -1
  103. package/dist/dto/social-auth.dto.d.ts +4 -34
  104. package/dist/dto/social-auth.dto.d.ts.map +1 -1
  105. package/dist/dto/social-auth.dto.js +10 -68
  106. package/dist/dto/social-auth.dto.js.map +1 -1
  107. package/dist/dto/update-user-attributes.dto.d.ts +26 -0
  108. package/dist/dto/update-user-attributes.dto.d.ts.map +1 -0
  109. package/dist/dto/update-user-attributes.dto.js +30 -0
  110. package/dist/dto/update-user-attributes.dto.js.map +1 -0
  111. package/dist/index.d.ts +5 -0
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +5 -0
  114. package/dist/index.js.map +1 -1
  115. package/dist/interfaces/hooks.interface.d.ts +2 -1
  116. package/dist/interfaces/hooks.interface.d.ts.map +1 -1
  117. package/dist/interfaces/provider.interface.d.ts +1 -1
  118. package/dist/interfaces/provider.interface.d.ts.map +1 -1
  119. package/dist/services/adaptive-mfa-decision.service.js +2 -2
  120. package/dist/services/adaptive-mfa-decision.service.js.map +1 -1
  121. package/dist/services/admin-auth.service.d.ts +307 -0
  122. package/dist/services/admin-auth.service.d.ts.map +1 -0
  123. package/dist/services/admin-auth.service.js +885 -0
  124. package/dist/services/admin-auth.service.js.map +1 -0
  125. package/dist/services/auth-audit.service.d.ts +16 -16
  126. package/dist/services/auth-audit.service.d.ts.map +1 -1
  127. package/dist/services/auth-audit.service.js +33 -33
  128. package/dist/services/auth-audit.service.js.map +1 -1
  129. package/dist/services/auth-challenge-helper.service.js +3 -3
  130. package/dist/services/auth-challenge-helper.service.js.map +1 -1
  131. package/dist/services/auth-service-internal-helpers.d.ts +2 -2
  132. package/dist/services/auth-service-internal-helpers.d.ts.map +1 -1
  133. package/dist/services/auth-service-internal-helpers.js.map +1 -1
  134. package/dist/services/auth.service.d.ts +122 -438
  135. package/dist/services/auth.service.d.ts.map +1 -1
  136. package/dist/services/auth.service.js +424 -1255
  137. package/dist/services/auth.service.js.map +1 -1
  138. package/dist/services/mfa.service.d.ts +80 -12
  139. package/dist/services/mfa.service.d.ts.map +1 -1
  140. package/dist/services/mfa.service.js +347 -261
  141. package/dist/services/mfa.service.js.map +1 -1
  142. package/dist/services/social-auth.service.d.ts +7 -0
  143. package/dist/services/social-auth.service.d.ts.map +1 -1
  144. package/dist/services/social-auth.service.js +38 -26
  145. package/dist/services/social-auth.service.js.map +1 -1
  146. package/dist/services/user.service.d.ts +3 -3
  147. package/dist/services/user.service.d.ts.map +1 -1
  148. package/dist/services/user.service.js +7 -7
  149. package/dist/services/user.service.js.map +1 -1
  150. package/dist/utils/dto-validator.d.ts.map +1 -1
  151. package/dist/utils/dto-validator.js +50 -4
  152. package/dist/utils/dto-validator.js.map +1 -1
  153. package/dist/utils/setup/init-services.d.ts +2 -1
  154. package/dist/utils/setup/init-services.d.ts.map +1 -1
  155. package/dist/utils/setup/init-services.js +2 -0
  156. package/dist/utils/setup/init-services.js.map +1 -1
  157. package/package.json +1 -1
  158. package/dist/dto/change-password-request.dto.d.ts +0 -43
  159. package/dist/dto/change-password-request.dto.d.ts.map +0 -1
  160. package/dist/dto/change-password-request.dto.js.map +0 -1
  161. package/dist/dto/update-user-attributes-request.dto.d.ts +0 -44
  162. package/dist/dto/update-user-attributes-request.dto.d.ts.map +0 -1
  163. 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
  }
@@ -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');
2098
- }
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');
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;
1602
+ }
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,83 @@ class AuthService {
2625
2075
  });
2626
2076
  return { sessions: sessionInfos };
2627
2077
  }
2078
+ /**
2079
+ * Get MFA status for current authenticated user
2080
+ *
2081
+ * Returns comprehensive MFA status including enabled status, configured methods,
2082
+ * available methods, backup codes, and exemption information.
2083
+ *
2084
+ * Usage Patterns:
2085
+ * - **User viewing own MFA status**: User views their MFA configuration (protected endpoint)
2086
+ *
2087
+ * Security:
2088
+ * - Uses authenticated user context for sub
2089
+ * - Endpoint MUST be protected by authentication guards
2090
+ *
2091
+ * @returns MFA status response
2092
+ * @throws {NAuthException} FORBIDDEN if user not authenticated
2093
+ * @throws {NAuthException} NOT_FOUND if user not found
2094
+ *
2095
+ * @example User viewing own MFA status
2096
+ * ```typescript
2097
+ * @UseGuards(AuthGuard)
2098
+ * @Get('mfa/status')
2099
+ * async getMFAStatus() {
2100
+ * return this.authService.getMFAStatus();
2101
+ * }
2102
+ * ```
2103
+ */
2104
+ async getMFAStatus() {
2105
+ if (!this.mfaService) {
2106
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'MFA service is not available');
2107
+ }
2108
+ // Get current authenticated user from context
2109
+ const currentUser = this.getCurrentUserOrThrow();
2110
+ // Call MFA service with user's sub
2111
+ // Pass as plain object to ensure proper transformation by ensureValidatedDto
2112
+ return await this.mfaService.getMFAStatus({ sub: currentUser.sub });
2113
+ }
2114
+ /**
2115
+ * Get authentication audit history for current authenticated user
2116
+ *
2117
+ * Returns paginated audit trail of authentication events for the user:
2118
+ * - Login attempts (success/failure)
2119
+ * - Password changes
2120
+ * - MFA setup/verification
2121
+ * - Device trust events
2122
+ * - Device information, location, risk factors
2123
+ *
2124
+ * Usage Patterns:
2125
+ * - **User viewing own audit history**: User views their authentication history (protected endpoint)
2126
+ *
2127
+ * Security:
2128
+ * - Uses authenticated user context for sub
2129
+ * - Endpoint MUST be protected by authentication guards
2130
+ *
2131
+ * @param dto - Optional query parameters for filtering and pagination
2132
+ * @returns Paginated audit history response
2133
+ * @throws {NAuthException} FORBIDDEN if user not authenticated
2134
+ * @throws {NAuthException} NOT_FOUND if user not found
2135
+ *
2136
+ * @example User viewing own audit history
2137
+ * ```typescript
2138
+ * @UseGuards(AuthGuard)
2139
+ * @Get('audit/history')
2140
+ * async getAuditHistory(@Query() query: GetUserAuthHistoryDTO) {
2141
+ * return this.authService.getUserAuthHistory(query);
2142
+ * }
2143
+ * ```
2144
+ */
2145
+ async getUserAuthHistory(dto = {}) {
2146
+ if (!this.auditService) {
2147
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'Audit service is not available');
2148
+ }
2149
+ // Get current authenticated user from context
2150
+ const currentUser = this.getCurrentUserOrThrow();
2151
+ // Convert to admin DTO with sub from context
2152
+ const adminDto = Object.assign(new admin_get_user_auth_history_dto_1.AdminGetUserAuthHistoryDTO(), { ...dto, sub: currentUser.sub });
2153
+ return await this.auditService.getUserAuthHistory(adminDto);
2154
+ }
2628
2155
  /**
2629
2156
  * Logout a specific session by ID
2630
2157
  *
@@ -2633,17 +2160,14 @@ class AuthService {
2633
2160
  * Useful for "sign out from device" functionality in user dashboards.
2634
2161
  *
2635
2162
  * 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)
2163
+ * - **User logging out own session**: User revokes specific session (protected endpoint)
2638
2164
  *
2639
2165
  * Security:
2640
- * - Requires explicit sub parameter
2166
+ * - Uses authenticated user context for sub
2641
2167
  * - 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
2168
+ * - Endpoint MUST be protected by authentication guards
2645
2169
  *
2646
- * @param dto - Contains sessionId and user sub
2170
+ * @param dto - Contains sessionId
2647
2171
  * @returns Success status and whether it was the current session
2648
2172
  * @throws {NAuthException} NOT_FOUND if user not found
2649
2173
  * @throws {NAuthException} SESSION_NOT_FOUND if session not found
@@ -2654,7 +2178,7 @@ class AuthService {
2654
2178
  * @UseGuards(AuthGuard)
2655
2179
  * @Delete('sessions/:sessionId')
2656
2180
  * async logoutSession(@CurrentUser() user: IUser, @Param('sessionId') sessionId: string) {
2657
- * return this.authService.logoutSession({ sub: user.sub, sessionId });
2181
+ * return this.authService.logoutSession({ sessionId });
2658
2182
  * }
2659
2183
  * ```
2660
2184
  *
@@ -2663,15 +2187,17 @@ class AuthService {
2663
2187
  * @UseGuards(AuthGuard, AdminGuard)
2664
2188
  * @Delete('admin/users/:sub/sessions/:sessionId')
2665
2189
  * async adminRevokeSession(@Param('sub') sub: string, @Param('sessionId') sessionId: string) {
2666
- * return this.authService.logoutSession({ sub, sessionId });
2190
+ * return this.adminAuthService.revokeUserSession({ sub, sessionId });
2667
2191
  * }
2668
2192
  * ```
2669
2193
  */
2670
2194
  async logoutSession(dto) {
2671
2195
  // Ensure DTO is validated (supports direct usage without framework validation)
2672
2196
  dto = await (0, dto_validator_1.ensureValidatedDto)(logout_session_dto_1.LogoutSessionDTO, dto);
2197
+ // Get current authenticated user from context
2198
+ const currentUser = this.getCurrentUserOrThrow();
2673
2199
  // Get user by sub to get internal id
2674
- const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
2200
+ const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
2675
2201
  if (!user) {
2676
2202
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2677
2203
  }
@@ -2695,7 +2221,7 @@ class AuthService {
2695
2221
  const wasCurrentSession = currentSessionId !== null && sessionId === currentSessionId;
2696
2222
  // Revoke the session
2697
2223
  await this.sessionService.revokeSession(sessionId, 'User requested logout', {
2698
- requestedBy: dto.sub,
2224
+ requestedBy: currentUser.sub,
2699
2225
  wasCurrentSession,
2700
2226
  });
2701
2227
  // Clear cookies if this was the current session
@@ -2745,14 +2271,13 @@ class AuthService {
2745
2271
  * checks password reuse policy, and updates the user's password hash and history.
2746
2272
  * Executes configured pre-change hooks if provided.
2747
2273
  *
2748
- * @param sub - External user identifier (sub/UUID)
2749
2274
  * @param dto - ChangePasswordDTO containing old and new password
2750
2275
  * @returns void
2751
2276
  * @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
2277
  *
2753
2278
  * @example
2754
2279
  * ```typescript
2755
- * await authService.changePassword('user-uuid', {
2280
+ * await authService.changePassword({
2756
2281
  * oldPassword: 'currentPass123!',
2757
2282
  * newPassword: 'newStr0ngPass!@#',
2758
2283
  * });
@@ -2760,30 +2285,42 @@ class AuthService {
2760
2285
  */
2761
2286
  async changePassword(dto) {
2762
2287
  // 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);
2288
+ dto = await (0, dto_validator_1.ensureValidatedDto)(change_password_dto_1.ChangePasswordDTO, dto);
2289
+ // Get current authenticated user from context
2290
+ const currentUser = this.getCurrentUserOrThrow();
2764
2291
  // Get user by sub
2765
- const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
2766
- if (!user || !user.passwordHash) {
2292
+ const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
2293
+ if (!user) {
2767
2294
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
2768
2295
  }
2769
2296
  // ============================================================================
2297
+ // Social-only accounts: allow setting first password without old password
2298
+ // ============================================================================
2299
+ if (!user.passwordHash) {
2300
+ if (dto.oldPassword !== '') {
2301
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password change not allowed');
2302
+ }
2303
+ }
2304
+ // ============================================================================
2770
2305
  // Lifecycle Hook: beforePasswordChange (TODO: Implement provider-based hook)
2771
2306
  // ============================================================================
2772
2307
  // TODO: Implement provider-based hook for beforePasswordChange
2773
- // const allowed = await this.hookRegistry.executeBeforePasswordChange(dto.sub, dto.oldPassword);
2308
+ // const allowed = await this.hookRegistry.executeBeforePasswordChange(currentUser.sub, dto.oldPassword);
2774
2309
  // if (!allowed) {
2775
2310
  // throw new NAuthException(AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password change not allowed');
2776
2311
  // }
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');
2312
+ if (user.passwordHash) {
2313
+ // Verify old password
2314
+ const isValid = await this.passwordService.verifyPassword(dto.oldPassword, user.passwordHash);
2315
+ if (!isValid) {
2316
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_INCORRECT, 'Current password is incorrect');
2317
+ }
2781
2318
  }
2782
2319
  // ============================================================================
2783
2320
  // Lifecycle Hook: afterPasswordChange (TODO: Implement provider-based hook)
2784
2321
  // ============================================================================
2785
2322
  // TODO: Implement provider-based hook for afterPasswordChange
2786
- // await this.hookRegistry.executeAfterPasswordChange(dto.sub);
2323
+ // await this.hookRegistry.executeAfterPasswordChange(currentUser.sub);
2787
2324
  await this.helpers.updateUserPassword({
2788
2325
  user,
2789
2326
  newPassword: dto.newPassword,
@@ -2802,53 +2339,38 @@ class AuthService {
2802
2339
  *
2803
2340
  * Updates user fields (name, email, phone, username, metadata) and enforces unique constraints and verification rules.
2804
2341
  *
2805
- * @param dto - UpdateUserAttributesRequestDTO containing sub and fields to update
2342
+ * @param dto - UpdateUserAttributesDTO containing fields to update
2806
2343
  * @returns Updated user object
2807
2344
  * @throws {NAuthException} If user not found or unique constraint violated
2808
2345
  *
2809
2346
  * @example
2810
- * await authService.updateUserAttributes({ sub: 'user-uuid', email: 'test@example.com' });
2347
+ * await authService.updateUserAttributes({ email: 'test@example.com' });
2811
2348
  */
2812
2349
  async updateUserAttributes(dto) {
2813
- return await this.userService.updateUserAttributes(dto);
2350
+ const currentUser = this.getCurrentUserOrThrow();
2351
+ const adminDto = Object.assign(new admin_update_user_attributes_dto_1.AdminUpdateUserAttributesDTO(), { sub: currentUser.sub, ...dto });
2352
+ return await this.userService.updateUserAttributes(adminDto);
2814
2353
  }
2815
2354
  /**
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.
2355
+ * Get user for authentication context
2820
2356
  *
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)
2357
+ * Loads user by sub (external identifier) with all fields needed for auth context.
2358
+ * Computes hasPasswordHash from passwordHash, then removes passwordHash and other sensitive fields.
2825
2359
  *
2826
- * Audit:
2827
- * - Records EMAIL_VERIFIED or PHONE_VERIFIED audit events
2828
- * - Includes performedBy from authenticated admin context
2360
+ * This method is used by AuthHandler and AuthGuard to load authenticated users.
2361
+ * It ensures consistent user object shape across platforms (core + NestJS).
2829
2362
  *
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
2363
+ * @param sub - External user identifier (UUID)
2364
+ * @returns User object with hasPasswordHash flag, without sensitive fields
2365
+ * @throws {NAuthException} If user not found or account is inactive
2833
2366
  *
2834
2367
  * @example
2835
2368
  * ```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
- * });
2369
+ * const user = await authService.getUserForAuthContext('user-uuid');
2848
2370
  * ```
2849
2371
  */
2850
- async updateVerifiedStatus(dto) {
2851
- return await this.userService.updateVerifiedStatus(dto);
2372
+ async getUserForAuthContext(sub) {
2373
+ return await this.userService.getUserForAuthContext(sub);
2852
2374
  }
2853
2375
  /**
2854
2376
  * Validate JWT access token
@@ -2886,7 +2408,6 @@ class AuthService {
2886
2408
  * ```
2887
2409
  */
2888
2410
  async validateAccessToken(dto) {
2889
- // Ensure DTO is validated (supports direct usage without framework validation)
2890
2411
  dto = await (0, dto_validator_1.ensureValidatedDto)(validate_access_token_dto_1.ValidateAccessTokenDTO, dto);
2891
2412
  const result = await this.jwtService.validateAccessToken(dto.accessToken);
2892
2413
  return {
@@ -2897,368 +2418,6 @@ class AuthService {
2897
2418
  };
2898
2419
  }
2899
2420
  // ============================================================================
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
2421
  // Forgot Password (Account Recovery)
3263
2422
  // ============================================================================
3264
2423
  /**
@@ -3394,6 +2553,16 @@ class AuthService {
3394
2553
  }, this.passwordService, this.auditService);
3395
2554
  return { success: true, mustChangePassword: false };
3396
2555
  }
2556
+ // ============================================================================
2557
+ // Helper Methods
2558
+ // ============================================================================
2559
+ getCurrentUserOrThrow() {
2560
+ const currentUser = context_storage_1.ContextStorage.get('CURRENT_USER');
2561
+ if (!currentUser) {
2562
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.FORBIDDEN, 'Authentication required');
2563
+ }
2564
+ return currentUser;
2565
+ }
3397
2566
  }
3398
2567
  exports.AuthService = AuthService;
3399
2568
  //# sourceMappingURL=auth.service.js.map