@nauth-toolkit/core 0.1.87 → 0.1.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dto/admin-get-mfa-status.dto.d.ts +20 -0
- package/dist/dto/admin-get-mfa-status.dto.d.ts.map +1 -0
- package/dist/dto/{change-password-request.dto.js → admin-get-mfa-status.dto.js} +22 -32
- package/dist/dto/admin-get-mfa-status.dto.js.map +1 -0
- package/dist/dto/admin-get-user-auth-history.dto.d.ts +62 -0
- package/dist/dto/admin-get-user-auth-history.dto.d.ts.map +1 -0
- package/dist/dto/admin-get-user-auth-history.dto.js +87 -0
- package/dist/dto/admin-get-user-auth-history.dto.js.map +1 -0
- package/dist/dto/admin-logout-all.dto.d.ts +48 -0
- package/dist/dto/admin-logout-all.dto.d.ts.map +1 -0
- package/dist/dto/admin-logout-all.dto.js +85 -0
- package/dist/dto/admin-logout-all.dto.js.map +1 -0
- package/dist/dto/admin-remove-devices.dto.d.ts +25 -0
- package/dist/dto/admin-remove-devices.dto.d.ts.map +1 -0
- package/dist/dto/admin-remove-devices.dto.js +50 -0
- package/dist/dto/admin-remove-devices.dto.js.map +1 -0
- package/dist/dto/admin-reset-password.dto.d.ts +15 -19
- package/dist/dto/admin-reset-password.dto.d.ts.map +1 -1
- package/dist/dto/admin-reset-password.dto.js +21 -41
- package/dist/dto/admin-reset-password.dto.js.map +1 -1
- package/dist/dto/admin-revoke-session.dto.d.ts +22 -0
- package/dist/dto/admin-revoke-session.dto.d.ts.map +1 -0
- package/dist/dto/admin-revoke-session.dto.js +48 -0
- package/dist/dto/admin-revoke-session.dto.js.map +1 -0
- package/dist/dto/admin-set-password.dto.d.ts +8 -10
- package/dist/dto/admin-set-password.dto.d.ts.map +1 -1
- package/dist/dto/admin-set-password.dto.js +11 -21
- package/dist/dto/admin-set-password.dto.js.map +1 -1
- package/dist/dto/admin-set-preferred-method.dto.d.ts +25 -0
- package/dist/dto/admin-set-preferred-method.dto.d.ts.map +1 -0
- package/dist/dto/admin-set-preferred-method.dto.js +50 -0
- package/dist/dto/admin-set-preferred-method.dto.js.map +1 -0
- package/dist/dto/admin-update-user-attributes.dto.d.ts +41 -0
- package/dist/dto/admin-update-user-attributes.dto.d.ts.map +1 -0
- package/dist/dto/{update-user-attributes-request.dto.js → admin-update-user-attributes.dto.js} +12 -17
- package/dist/dto/admin-update-user-attributes.dto.js.map +1 -0
- package/dist/dto/auth-challenge.dto.d.ts +2 -2
- package/dist/dto/auth-challenge.dto.d.ts.map +1 -1
- package/dist/dto/auth-challenge.dto.js +3 -3
- package/dist/dto/auth-challenge.dto.js.map +1 -1
- package/dist/dto/auth-response.dto.d.ts +1 -1
- package/dist/dto/auth-response.dto.d.ts.map +1 -1
- package/dist/dto/auth-response.dto.js +1 -1
- package/dist/dto/auth-response.dto.js.map +1 -1
- package/dist/dto/get-mfa-status.dto.d.ts +3 -32
- package/dist/dto/get-mfa-status.dto.d.ts.map +1 -1
- package/dist/dto/get-mfa-status.dto.js +4 -55
- package/dist/dto/get-mfa-status.dto.js.map +1 -1
- package/dist/dto/get-risk-assessment-history.dto.d.ts +3 -3
- package/dist/dto/get-risk-assessment-history.dto.d.ts.map +1 -1
- package/dist/dto/get-risk-assessment-history.dto.js +5 -5
- package/dist/dto/get-risk-assessment-history.dto.js.map +1 -1
- package/dist/dto/get-suspicious-activity.dto.d.ts +3 -3
- package/dist/dto/get-suspicious-activity.dto.d.ts.map +1 -1
- package/dist/dto/get-suspicious-activity.dto.js +5 -5
- package/dist/dto/get-suspicious-activity.dto.js.map +1 -1
- package/dist/dto/get-user-auth-history.dto.d.ts +4 -39
- package/dist/dto/get-user-auth-history.dto.d.ts.map +1 -1
- package/dist/dto/get-user-auth-history.dto.js +53 -51
- package/dist/dto/get-user-auth-history.dto.js.map +1 -1
- package/dist/dto/get-user-devices.dto.d.ts +5 -18
- package/dist/dto/get-user-devices.dto.d.ts.map +1 -1
- package/dist/dto/get-user-devices.dto.js +5 -39
- package/dist/dto/get-user-devices.dto.js.map +1 -1
- package/dist/dto/get-user-sessions-response.dto.d.ts +1 -1
- package/dist/dto/get-user-sessions-response.dto.js +1 -1
- package/dist/dto/get-user-sessions.dto.d.ts +1 -1
- package/dist/dto/get-user-sessions.dto.js +1 -1
- package/dist/dto/index.d.ts +9 -2
- package/dist/dto/index.d.ts.map +1 -1
- package/dist/dto/index.js +9 -2
- package/dist/dto/index.js.map +1 -1
- package/dist/dto/logout-all-response.dto.d.ts +1 -1
- package/dist/dto/logout-all-response.dto.js +1 -1
- package/dist/dto/logout-all.dto.d.ts +1 -18
- package/dist/dto/logout-all.dto.d.ts.map +1 -1
- package/dist/dto/logout-all.dto.js +1 -30
- package/dist/dto/logout-all.dto.js.map +1 -1
- package/dist/dto/logout-session.dto.d.ts +0 -5
- package/dist/dto/logout-session.dto.d.ts.map +1 -1
- package/dist/dto/logout-session.dto.js +0 -12
- package/dist/dto/logout-session.dto.js.map +1 -1
- package/dist/dto/logout.dto.d.ts +1 -18
- package/dist/dto/logout.dto.d.ts.map +1 -1
- package/dist/dto/logout.dto.js +1 -30
- package/dist/dto/logout.dto.js.map +1 -1
- package/dist/dto/remove-devices.dto.d.ts +4 -16
- package/dist/dto/remove-devices.dto.d.ts.map +1 -1
- package/dist/dto/remove-devices.dto.js +4 -26
- package/dist/dto/remove-devices.dto.js.map +1 -1
- package/dist/dto/set-mfa-exemption.dto.d.ts +8 -9
- package/dist/dto/set-mfa-exemption.dto.d.ts.map +1 -1
- package/dist/dto/set-mfa-exemption.dto.js +11 -13
- package/dist/dto/set-mfa-exemption.dto.js.map +1 -1
- package/dist/dto/set-must-change-password.dto.d.ts +3 -3
- package/dist/dto/set-must-change-password.dto.d.ts.map +1 -1
- package/dist/dto/set-must-change-password.dto.js +5 -5
- package/dist/dto/set-must-change-password.dto.js.map +1 -1
- package/dist/dto/set-preferred-method.dto.d.ts +4 -16
- package/dist/dto/set-preferred-method.dto.d.ts.map +1 -1
- package/dist/dto/set-preferred-method.dto.js +4 -26
- package/dist/dto/set-preferred-method.dto.js.map +1 -1
- package/dist/dto/setup-mfa.dto.d.ts +3 -18
- package/dist/dto/setup-mfa.dto.d.ts.map +1 -1
- package/dist/dto/setup-mfa.dto.js +3 -30
- package/dist/dto/setup-mfa.dto.js.map +1 -1
- package/dist/dto/social-auth.dto.d.ts +4 -34
- package/dist/dto/social-auth.dto.d.ts.map +1 -1
- package/dist/dto/social-auth.dto.js +10 -68
- package/dist/dto/social-auth.dto.js.map +1 -1
- package/dist/dto/update-user-attributes.dto.d.ts +26 -0
- package/dist/dto/update-user-attributes.dto.d.ts.map +1 -0
- package/dist/dto/update-user-attributes.dto.js +30 -0
- package/dist/dto/update-user-attributes.dto.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/hooks.interface.d.ts +2 -1
- package/dist/interfaces/hooks.interface.d.ts.map +1 -1
- package/dist/interfaces/mfa-provider.interface.d.ts +7 -8
- package/dist/interfaces/mfa-provider.interface.d.ts.map +1 -1
- package/dist/interfaces/provider.interface.d.ts +1 -1
- package/dist/interfaces/provider.interface.d.ts.map +1 -1
- package/dist/services/adaptive-mfa-decision.service.js +2 -2
- package/dist/services/adaptive-mfa-decision.service.js.map +1 -1
- package/dist/services/admin-auth.service.d.ts +307 -0
- package/dist/services/admin-auth.service.d.ts.map +1 -0
- package/dist/services/admin-auth.service.js +885 -0
- package/dist/services/admin-auth.service.js.map +1 -0
- package/dist/services/auth-audit.service.d.ts +16 -16
- package/dist/services/auth-audit.service.d.ts.map +1 -1
- package/dist/services/auth-audit.service.js +33 -33
- package/dist/services/auth-audit.service.js.map +1 -1
- package/dist/services/auth-challenge-helper.service.js +3 -3
- package/dist/services/auth-challenge-helper.service.js.map +1 -1
- package/dist/services/auth-service-internal-helpers.d.ts +13 -2
- package/dist/services/auth-service-internal-helpers.d.ts.map +1 -1
- package/dist/services/auth-service-internal-helpers.js +39 -1
- package/dist/services/auth-service-internal-helpers.js.map +1 -1
- package/dist/services/auth.service.d.ts +94 -438
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +388 -1255
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/mfa-base.service.d.ts +14 -4
- package/dist/services/mfa-base.service.d.ts.map +1 -1
- package/dist/services/mfa-base.service.js +22 -1
- package/dist/services/mfa-base.service.js.map +1 -1
- package/dist/services/mfa.service.d.ts +107 -33
- package/dist/services/mfa.service.d.ts.map +1 -1
- package/dist/services/mfa.service.js +456 -333
- package/dist/services/mfa.service.js.map +1 -1
- package/dist/services/social-auth.service.d.ts +7 -0
- package/dist/services/social-auth.service.d.ts.map +1 -1
- package/dist/services/social-auth.service.js +38 -26
- package/dist/services/social-auth.service.js.map +1 -1
- package/dist/services/user.service.d.ts +3 -3
- package/dist/services/user.service.d.ts.map +1 -1
- package/dist/services/user.service.js +7 -7
- package/dist/services/user.service.js.map +1 -1
- package/dist/utils/dto-validator.d.ts.map +1 -1
- package/dist/utils/dto-validator.js +50 -4
- package/dist/utils/dto-validator.js.map +1 -1
- package/dist/utils/setup/init-services.d.ts +2 -1
- package/dist/utils/setup/init-services.d.ts.map +1 -1
- package/dist/utils/setup/init-services.js +2 -0
- package/dist/utils/setup/init-services.js.map +1 -1
- package/package.json +1 -1
- package/dist/dto/change-password-request.dto.d.ts +0 -43
- package/dist/dto/change-password-request.dto.d.ts.map +0 -1
- package/dist/dto/change-password-request.dto.js.map +0 -1
- package/dist/dto/update-user-attributes-request.dto.d.ts +0 -44
- package/dist/dto/update-user-attributes-request.dto.d.ts.map +0 -1
- 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
|
-
|
|
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
|
|
45
|
-
const
|
|
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
|
|
56
|
-
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
975
|
+
sub: user.sub,
|
|
1544
976
|
});
|
|
1545
977
|
});
|
|
1546
978
|
}
|
|
@@ -1774,7 +1206,7 @@ class AuthService {
|
|
|
1774
1206
|
if (!provider.sendChallenge) {
|
|
1775
1207
|
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `${method.toUpperCase()} MFA provider does not support sending challenges`);
|
|
1776
1208
|
}
|
|
1777
|
-
const result = await provider.sendChallenge(
|
|
1209
|
+
const result = await provider.sendChallenge?.();
|
|
1778
1210
|
this.logger?.debug?.(`${method.toUpperCase()} MFA code resent: user=${user.sub}`);
|
|
1779
1211
|
// Provider returns masked phone or email
|
|
1780
1212
|
return { destination: result };
|
|
@@ -1942,238 +1374,255 @@ class AuthService {
|
|
|
1942
1374
|
* ```
|
|
1943
1375
|
*/
|
|
1944
1376
|
async refreshToken(dto) {
|
|
1945
|
-
// Ensure DTO is validated (supports direct usage without framework validation)
|
|
1946
|
-
dto = await (0, dto_validator_1.ensureValidatedDto)(refresh_token_dto_1.RefreshTokenDTO, dto);
|
|
1947
|
-
// After validation, refreshToken must be present (validation ensures it's a valid string)
|
|
1948
|
-
// Controller should have filled it from cookies if it was missing in cookies mode
|
|
1949
|
-
if (!dto.refreshToken) {
|
|
1950
|
-
// Best-effort: clear cookies in cookie/hybrid delivery so clients don't keep sending stale cookies.
|
|
1951
|
-
this.clearAuthCookiesOnRefreshFailure(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID);
|
|
1952
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Refresh token is required');
|
|
1953
|
-
}
|
|
1954
|
-
// Extract to const for type narrowing (TypeScript doesn't narrow optional properties)
|
|
1955
|
-
const refreshToken = dto.refreshToken;
|
|
1956
|
-
const tokenHash = this.jwtService.hashToken(refreshToken);
|
|
1957
|
-
// ============================================================================
|
|
1958
|
-
// CRITICAL SECURITY FIX #1 & #2: Distributed Lock + Reuse Detection
|
|
1959
|
-
// ============================================================================
|
|
1960
|
-
// CRITICAL: We need to get session ID for locking, but we must lock BEFORE validation
|
|
1961
|
-
// to prevent race conditions. So we do a quick, lightweight lookup first.
|
|
1962
|
-
// Find session by refresh token hash - this is fast and allows us to get session ID
|
|
1963
|
-
const session = await this.sessionService.findByRefreshToken(tokenHash);
|
|
1964
|
-
if (!session || session.isRevoked) {
|
|
1965
|
-
// Validate token to get user info for error message
|
|
1966
|
-
const validation = await this.jwtService.validateRefreshToken(refreshToken);
|
|
1967
|
-
const userId = validation.payload?.sub || 'unknown';
|
|
1968
|
-
this.logger?.debug?.(`Session not found or revoked for user ${userId}. Possible issue where token are not cleared on logout`);
|
|
1969
|
-
// Best-effort: clear cookies in cookie/hybrid delivery so clients don't keep sending stale cookies.
|
|
1970
|
-
this.clearAuthCookiesOnRefreshFailure(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND);
|
|
1971
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
|
|
1972
|
-
}
|
|
1973
|
-
// Acquire distributed lock using SESSION ID (not token hash)
|
|
1974
|
-
// THIS MUST HAPPEN BEFORE VALIDATION to prevent race conditions
|
|
1975
|
-
// where multiple requests validate the same token before any lock is acquired
|
|
1976
|
-
const lockKey = `session-refresh:${session.id}`;
|
|
1977
|
-
this.logger?.debug?.(`[REFRESH DEBUG] Attempting to acquire lock ${lockKey} for token hash ${tokenHash.substring(0, 16)}...`);
|
|
1978
|
-
let lockAcquired = false;
|
|
1979
1377
|
try {
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
if
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
-
|
|
1990
|
-
//
|
|
1991
|
-
//
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
//
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
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
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
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: {
|
|
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:
|
|
2059
|
-
riskFactors: [risk_factor_enum_1.RiskFactor.
|
|
2060
|
-
reason: '
|
|
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
|
|
1558
|
+
description: 'Refresh token reuse attempt detected via atomic operation. Legitimate user session preserved.',
|
|
2063
1559
|
metadata: {
|
|
2064
|
-
|
|
2065
|
-
tokenSessionId,
|
|
2066
|
-
tokenHash: `${tokenHash.substring(0, 16)}...`,
|
|
1560
|
+
tokenFamily: payload.tokenFamily,
|
|
2067
1561
|
detectedAt: new Date().toISOString(),
|
|
2068
|
-
action: '
|
|
1562
|
+
action: 'reuse_blocked_atomic',
|
|
2069
1563
|
},
|
|
2070
1564
|
});
|
|
2071
1565
|
}
|
|
2072
1566
|
}
|
|
2073
1567
|
catch (auditError) {
|
|
2074
|
-
|
|
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
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const payload = validation.payload;
|
|
2091
|
-
// Re-check session after acquiring lock (it might have been revoked/updated)
|
|
2092
|
-
// Since we have the lock, no other request can modify this session, but it might have been revoked
|
|
2093
|
-
// We already have currentSession from the early reuse check, but re-fetch to ensure it's still valid
|
|
2094
|
-
const lockedSession = (await this.sessionService.findByIdLight(session.id));
|
|
2095
|
-
if (!lockedSession || lockedSession.isRevoked || lockedSession.id !== session.id) {
|
|
2096
|
-
this.logger?.debug?.(`Session changed after lock acquisition for user ${payload.sub}. Session may have been revoked.`);
|
|
2097
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SESSION_NOT_FOUND, 'Session not found or revoked');
|
|
1596
|
+
catch (error) {
|
|
1597
|
+
// Best-effort cookie cleanup for session-invalid refresh errors.
|
|
1598
|
+
if (error instanceof nauth_exception_1.NAuthException) {
|
|
1599
|
+
this.clearAuthCookiesOnRefreshFailure(error.code);
|
|
1600
|
+
}
|
|
1601
|
+
throw error;
|
|
2098
1602
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
if (this.config.jwt.refreshToken.reuseDetection) {
|
|
2106
|
-
const refreshTokenTTL = this.jwtService.getRefreshTokenTTL();
|
|
2107
|
-
const marked = await this.sessionService.markRefreshTokenAsUsed(tokenHash, refreshTokenTTL);
|
|
2108
|
-
if (!marked) {
|
|
2109
|
-
// Token was already marked as used - reuse detected!
|
|
2110
|
-
this.logger?.error?.(`Token reuse detected for user ${payload.sub} - atomic mark failed, revoking entire token family ${payload.tokenFamily}`);
|
|
2111
|
-
// Audit the reuse attempt
|
|
2112
|
-
try {
|
|
2113
|
-
const userForAudit = (await this.userRepository.findOne({
|
|
2114
|
-
where: { sub: payload.sub },
|
|
2115
|
-
}));
|
|
2116
|
-
if (userForAudit) {
|
|
2117
|
-
await this.auditService?.recordEvent({
|
|
2118
|
-
userId: userForAudit.id,
|
|
2119
|
-
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.SUSPICIOUS_ACTIVITY,
|
|
2120
|
-
eventStatus: 'SUSPICIOUS',
|
|
2121
|
-
riskFactor: 75,
|
|
2122
|
-
riskFactors: [risk_factor_enum_1.RiskFactor.TOKEN_REUSE_ATTEMPT],
|
|
2123
|
-
reason: 'Token reuse attempt blocked',
|
|
2124
|
-
// Client info automatically included from context
|
|
2125
|
-
description: 'Refresh token reuse attempt detected via atomic operation. Legitimate user session preserved.',
|
|
2126
|
-
metadata: {
|
|
2127
|
-
tokenFamily: payload.tokenFamily,
|
|
2128
|
-
detectedAt: new Date().toISOString(),
|
|
2129
|
-
action: 'reuse_blocked_atomic',
|
|
2130
|
-
},
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
catch (auditError) {
|
|
2135
|
-
this.logger?.warn?.('Failed to record SUSPICIOUS_ACTIVITY audit event', { error: auditError });
|
|
2136
|
-
}
|
|
2137
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.TOKEN_INVALID, 'Refresh token has already been used');
|
|
1603
|
+
finally {
|
|
1604
|
+
// Always release lock, even if error occurs
|
|
1605
|
+
// Only release if we successfully acquired it
|
|
1606
|
+
if (lockAcquired) {
|
|
1607
|
+
await this.sessionService.releaseRefreshLock(lockKey);
|
|
1608
|
+
this.logger?.debug?.(`[REFRESH DEBUG] Released lock ${lockKey}`);
|
|
2138
1609
|
}
|
|
2139
|
-
this.logger?.debug?.(`Marked refresh token as used for session ${lockedSession.id}`);
|
|
2140
1610
|
}
|
|
2141
|
-
// Generate new token pair with same family
|
|
2142
|
-
// Note: deviceId not included in token - session.deviceId is source of truth
|
|
2143
|
-
const newTokens = await this.jwtService.generateTokenPair({
|
|
2144
|
-
userId: payload.sub,
|
|
2145
|
-
email: payload.email,
|
|
2146
|
-
sessionId: lockedSession.id.toString(), // Convert integer to string for JWT
|
|
2147
|
-
tokenFamily: payload.tokenFamily,
|
|
2148
|
-
});
|
|
2149
|
-
// Update session with new token hashes (token rotation)
|
|
2150
|
-
// This automatically invalidates the old tokens as they won't match the session
|
|
2151
|
-
await this.sessionService.updateTokens(lockedSession.id, this.jwtService.hashToken(newTokens.accessToken), this.jwtService.hashToken(newTokens.refreshToken));
|
|
2152
|
-
this.logger?.log?.(`Token refreshed successfully for user ${payload.sub}`);
|
|
2153
|
-
// Decode new tokens to get expiry times
|
|
2154
|
-
const accessTokenValidation = await this.jwtService.validateAccessToken(newTokens.accessToken);
|
|
2155
|
-
const refreshTokenValidation = await this.jwtService.validateRefreshToken(newTokens.refreshToken);
|
|
2156
|
-
return {
|
|
2157
|
-
accessToken: newTokens.accessToken,
|
|
2158
|
-
refreshToken: newTokens.refreshToken,
|
|
2159
|
-
accessTokenExpiresAt: accessTokenValidation.payload?.exp || 0,
|
|
2160
|
-
refreshTokenExpiresAt: refreshTokenValidation.payload?.exp || 0,
|
|
2161
|
-
};
|
|
2162
1611
|
}
|
|
2163
1612
|
catch (error) {
|
|
2164
|
-
//
|
|
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
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
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
|
-
|
|
2211
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
* -
|
|
2376
|
-
* -
|
|
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 -
|
|
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({
|
|
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
|
-
* //
|
|
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.
|
|
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:
|
|
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
|
|
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
|
-
* -
|
|
2550
|
-
* -
|
|
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(
|
|
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.
|
|
2022
|
+
* return this.adminAuthService.getUserSessions({ sub });
|
|
2573
2023
|
* }
|
|
2574
2024
|
* ```
|
|
2575
2025
|
*/
|
|
2576
|
-
async getUserSessions(
|
|
2577
|
-
//
|
|
2578
|
-
|
|
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:
|
|
2030
|
+
const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
|
|
2581
2031
|
if (!user) {
|
|
2582
2032
|
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
2583
2033
|
}
|
|
@@ -2625,6 +2075,47 @@ class AuthService {
|
|
|
2625
2075
|
});
|
|
2626
2076
|
return { sessions: sessionInfos };
|
|
2627
2077
|
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Get authentication audit history for current authenticated user
|
|
2080
|
+
*
|
|
2081
|
+
* Returns paginated audit trail of authentication events for the user:
|
|
2082
|
+
* - Login attempts (success/failure)
|
|
2083
|
+
* - Password changes
|
|
2084
|
+
* - MFA setup/verification
|
|
2085
|
+
* - Device trust events
|
|
2086
|
+
* - Device information, location, risk factors
|
|
2087
|
+
*
|
|
2088
|
+
* Usage Patterns:
|
|
2089
|
+
* - **User viewing own audit history**: User views their authentication history (protected endpoint)
|
|
2090
|
+
*
|
|
2091
|
+
* Security:
|
|
2092
|
+
* - Uses authenticated user context for sub
|
|
2093
|
+
* - Endpoint MUST be protected by authentication guards
|
|
2094
|
+
*
|
|
2095
|
+
* @param dto - Optional query parameters for filtering and pagination
|
|
2096
|
+
* @returns Paginated audit history response
|
|
2097
|
+
* @throws {NAuthException} FORBIDDEN if user not authenticated
|
|
2098
|
+
* @throws {NAuthException} NOT_FOUND if user not found
|
|
2099
|
+
*
|
|
2100
|
+
* @example User viewing own audit history
|
|
2101
|
+
* ```typescript
|
|
2102
|
+
* @UseGuards(AuthGuard)
|
|
2103
|
+
* @Get('audit/history')
|
|
2104
|
+
* async getAuditHistory(@Query() query: GetUserAuthHistoryDTO) {
|
|
2105
|
+
* return this.authService.getUserAuthHistory(query);
|
|
2106
|
+
* }
|
|
2107
|
+
* ```
|
|
2108
|
+
*/
|
|
2109
|
+
async getUserAuthHistory(dto = {}) {
|
|
2110
|
+
if (!this.auditService) {
|
|
2111
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'Audit service is not available');
|
|
2112
|
+
}
|
|
2113
|
+
// Get current authenticated user from context
|
|
2114
|
+
const currentUser = this.getCurrentUserOrThrow();
|
|
2115
|
+
// Convert to admin DTO with sub from context
|
|
2116
|
+
const adminDto = Object.assign(new admin_get_user_auth_history_dto_1.AdminGetUserAuthHistoryDTO(), { ...dto, sub: currentUser.sub });
|
|
2117
|
+
return await this.auditService.getUserAuthHistory(adminDto);
|
|
2118
|
+
}
|
|
2628
2119
|
/**
|
|
2629
2120
|
* Logout a specific session by ID
|
|
2630
2121
|
*
|
|
@@ -2633,17 +2124,14 @@ class AuthService {
|
|
|
2633
2124
|
* Useful for "sign out from device" functionality in user dashboards.
|
|
2634
2125
|
*
|
|
2635
2126
|
* Usage Patterns:
|
|
2636
|
-
* - **User logging out own session**: User revokes specific session (protected endpoint
|
|
2637
|
-
* - **Admin revoking any user's session**: Admin revokes specific session for any user (admin-protected endpoint, admin provides target user's sub)
|
|
2127
|
+
* - **User logging out own session**: User revokes specific session (protected endpoint)
|
|
2638
2128
|
*
|
|
2639
2129
|
* Security:
|
|
2640
|
-
* -
|
|
2130
|
+
* - Uses authenticated user context for sub
|
|
2641
2131
|
* - Validates session belongs to user (prevents unauthorized session revocation)
|
|
2642
|
-
* -
|
|
2643
|
-
* - For user endpoints: Extract sub from authenticated user context (@CurrentUser)
|
|
2644
|
-
* - For admin endpoints: Accept sub from route parameter and protect with admin guards
|
|
2132
|
+
* - Endpoint MUST be protected by authentication guards
|
|
2645
2133
|
*
|
|
2646
|
-
* @param dto - Contains sessionId
|
|
2134
|
+
* @param dto - Contains sessionId
|
|
2647
2135
|
* @returns Success status and whether it was the current session
|
|
2648
2136
|
* @throws {NAuthException} NOT_FOUND if user not found
|
|
2649
2137
|
* @throws {NAuthException} SESSION_NOT_FOUND if session not found
|
|
@@ -2654,7 +2142,7 @@ class AuthService {
|
|
|
2654
2142
|
* @UseGuards(AuthGuard)
|
|
2655
2143
|
* @Delete('sessions/:sessionId')
|
|
2656
2144
|
* async logoutSession(@CurrentUser() user: IUser, @Param('sessionId') sessionId: string) {
|
|
2657
|
-
* return this.authService.logoutSession({
|
|
2145
|
+
* return this.authService.logoutSession({ sessionId });
|
|
2658
2146
|
* }
|
|
2659
2147
|
* ```
|
|
2660
2148
|
*
|
|
@@ -2663,15 +2151,17 @@ class AuthService {
|
|
|
2663
2151
|
* @UseGuards(AuthGuard, AdminGuard)
|
|
2664
2152
|
* @Delete('admin/users/:sub/sessions/:sessionId')
|
|
2665
2153
|
* async adminRevokeSession(@Param('sub') sub: string, @Param('sessionId') sessionId: string) {
|
|
2666
|
-
* return this.
|
|
2154
|
+
* return this.adminAuthService.revokeUserSession({ sub, sessionId });
|
|
2667
2155
|
* }
|
|
2668
2156
|
* ```
|
|
2669
2157
|
*/
|
|
2670
2158
|
async logoutSession(dto) {
|
|
2671
2159
|
// Ensure DTO is validated (supports direct usage without framework validation)
|
|
2672
2160
|
dto = await (0, dto_validator_1.ensureValidatedDto)(logout_session_dto_1.LogoutSessionDTO, dto);
|
|
2161
|
+
// Get current authenticated user from context
|
|
2162
|
+
const currentUser = this.getCurrentUserOrThrow();
|
|
2673
2163
|
// Get user by sub to get internal id
|
|
2674
|
-
const user = (await this.userRepository.findOne({ where: { sub:
|
|
2164
|
+
const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
|
|
2675
2165
|
if (!user) {
|
|
2676
2166
|
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
2677
2167
|
}
|
|
@@ -2695,7 +2185,7 @@ class AuthService {
|
|
|
2695
2185
|
const wasCurrentSession = currentSessionId !== null && sessionId === currentSessionId;
|
|
2696
2186
|
// Revoke the session
|
|
2697
2187
|
await this.sessionService.revokeSession(sessionId, 'User requested logout', {
|
|
2698
|
-
requestedBy:
|
|
2188
|
+
requestedBy: currentUser.sub,
|
|
2699
2189
|
wasCurrentSession,
|
|
2700
2190
|
});
|
|
2701
2191
|
// Clear cookies if this was the current session
|
|
@@ -2745,14 +2235,13 @@ class AuthService {
|
|
|
2745
2235
|
* checks password reuse policy, and updates the user's password hash and history.
|
|
2746
2236
|
* Executes configured pre-change hooks if provided.
|
|
2747
2237
|
*
|
|
2748
|
-
* @param sub - External user identifier (sub/UUID)
|
|
2749
2238
|
* @param dto - ChangePasswordDTO containing old and new password
|
|
2750
2239
|
* @returns void
|
|
2751
2240
|
* @throws {NAuthException} If the user is not found, current password is incorrect, the new password is weak, password reuse is detected, or password change is disallowed by hooks.
|
|
2752
2241
|
*
|
|
2753
2242
|
* @example
|
|
2754
2243
|
* ```typescript
|
|
2755
|
-
* await authService.changePassword(
|
|
2244
|
+
* await authService.changePassword({
|
|
2756
2245
|
* oldPassword: 'currentPass123!',
|
|
2757
2246
|
* newPassword: 'newStr0ngPass!@#',
|
|
2758
2247
|
* });
|
|
@@ -2760,30 +2249,42 @@ class AuthService {
|
|
|
2760
2249
|
*/
|
|
2761
2250
|
async changePassword(dto) {
|
|
2762
2251
|
// Ensure DTO is validated (supports direct usage without framework validation)
|
|
2763
|
-
dto = await (0, dto_validator_1.ensureValidatedDto)(
|
|
2252
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(change_password_dto_1.ChangePasswordDTO, dto);
|
|
2253
|
+
// Get current authenticated user from context
|
|
2254
|
+
const currentUser = this.getCurrentUserOrThrow();
|
|
2764
2255
|
// Get user by sub
|
|
2765
|
-
const user = (await this.userRepository.findOne({ where: { sub:
|
|
2766
|
-
if (!user
|
|
2256
|
+
const user = (await this.userRepository.findOne({ where: { sub: currentUser.sub } }));
|
|
2257
|
+
if (!user) {
|
|
2767
2258
|
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
2768
2259
|
}
|
|
2769
2260
|
// ============================================================================
|
|
2261
|
+
// Social-only accounts: allow setting first password without old password
|
|
2262
|
+
// ============================================================================
|
|
2263
|
+
if (!user.passwordHash) {
|
|
2264
|
+
if (dto.oldPassword !== '') {
|
|
2265
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password change not allowed');
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
// ============================================================================
|
|
2770
2269
|
// Lifecycle Hook: beforePasswordChange (TODO: Implement provider-based hook)
|
|
2771
2270
|
// ============================================================================
|
|
2772
2271
|
// TODO: Implement provider-based hook for beforePasswordChange
|
|
2773
|
-
// const allowed = await this.hookRegistry.executeBeforePasswordChange(
|
|
2272
|
+
// const allowed = await this.hookRegistry.executeBeforePasswordChange(currentUser.sub, dto.oldPassword);
|
|
2774
2273
|
// if (!allowed) {
|
|
2775
2274
|
// throw new NAuthException(AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password change not allowed');
|
|
2776
2275
|
// }
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2276
|
+
if (user.passwordHash) {
|
|
2277
|
+
// Verify old password
|
|
2278
|
+
const isValid = await this.passwordService.verifyPassword(dto.oldPassword, user.passwordHash);
|
|
2279
|
+
if (!isValid) {
|
|
2280
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_INCORRECT, 'Current password is incorrect');
|
|
2281
|
+
}
|
|
2781
2282
|
}
|
|
2782
2283
|
// ============================================================================
|
|
2783
2284
|
// Lifecycle Hook: afterPasswordChange (TODO: Implement provider-based hook)
|
|
2784
2285
|
// ============================================================================
|
|
2785
2286
|
// TODO: Implement provider-based hook for afterPasswordChange
|
|
2786
|
-
// await this.hookRegistry.executeAfterPasswordChange(
|
|
2287
|
+
// await this.hookRegistry.executeAfterPasswordChange(currentUser.sub);
|
|
2787
2288
|
await this.helpers.updateUserPassword({
|
|
2788
2289
|
user,
|
|
2789
2290
|
newPassword: dto.newPassword,
|
|
@@ -2802,53 +2303,38 @@ class AuthService {
|
|
|
2802
2303
|
*
|
|
2803
2304
|
* Updates user fields (name, email, phone, username, metadata) and enforces unique constraints and verification rules.
|
|
2804
2305
|
*
|
|
2805
|
-
* @param dto -
|
|
2306
|
+
* @param dto - UpdateUserAttributesDTO containing fields to update
|
|
2806
2307
|
* @returns Updated user object
|
|
2807
2308
|
* @throws {NAuthException} If user not found or unique constraint violated
|
|
2808
2309
|
*
|
|
2809
2310
|
* @example
|
|
2810
|
-
* await authService.updateUserAttributes({
|
|
2311
|
+
* await authService.updateUserAttributes({ email: 'test@example.com' });
|
|
2811
2312
|
*/
|
|
2812
2313
|
async updateUserAttributes(dto) {
|
|
2813
|
-
|
|
2314
|
+
const currentUser = this.getCurrentUserOrThrow();
|
|
2315
|
+
const adminDto = Object.assign(new admin_update_user_attributes_dto_1.AdminUpdateUserAttributesDTO(), { sub: currentUser.sub, ...dto });
|
|
2316
|
+
return await this.userService.updateUserAttributes(adminDto);
|
|
2814
2317
|
}
|
|
2815
2318
|
/**
|
|
2816
|
-
*
|
|
2817
|
-
*
|
|
2818
|
-
* Intended for admin use cases such as migration or offline validation.
|
|
2819
|
-
* Updates verification status without requiring actual verification codes.
|
|
2319
|
+
* Get user for authentication context
|
|
2820
2320
|
*
|
|
2821
|
-
*
|
|
2822
|
-
*
|
|
2823
|
-
* - Can set verified=false even if email/phone doesn't exist (default state)
|
|
2824
|
-
* - Only updates provided fields (partial update)
|
|
2321
|
+
* Loads user by sub (external identifier) with all fields needed for auth context.
|
|
2322
|
+
* Computes hasPasswordHash from passwordHash, then removes passwordHash and other sensitive fields.
|
|
2825
2323
|
*
|
|
2826
|
-
*
|
|
2827
|
-
*
|
|
2828
|
-
* - Includes performedBy from authenticated admin context
|
|
2324
|
+
* This method is used by AuthHandler and AuthGuard to load authenticated users.
|
|
2325
|
+
* It ensures consistent user object shape across platforms (core + NestJS).
|
|
2829
2326
|
*
|
|
2830
|
-
* @param
|
|
2831
|
-
* @returns
|
|
2832
|
-
* @throws {NAuthException} If user not found or
|
|
2327
|
+
* @param sub - External user identifier (UUID)
|
|
2328
|
+
* @returns User object with hasPasswordHash flag, without sensitive fields
|
|
2329
|
+
* @throws {NAuthException} If user not found or account is inactive
|
|
2833
2330
|
*
|
|
2834
2331
|
* @example
|
|
2835
2332
|
* ```typescript
|
|
2836
|
-
*
|
|
2837
|
-
* await authService.updateVerifiedStatus({
|
|
2838
|
-
* sub: 'user-uuid',
|
|
2839
|
-
* isEmailVerified: true
|
|
2840
|
-
* });
|
|
2841
|
-
*
|
|
2842
|
-
* // Update both email and phone verification
|
|
2843
|
-
* await authService.updateVerifiedStatus({
|
|
2844
|
-
* sub: 'user-uuid',
|
|
2845
|
-
* isEmailVerified: true,
|
|
2846
|
-
* isPhoneVerified: false
|
|
2847
|
-
* });
|
|
2333
|
+
* const user = await authService.getUserForAuthContext('user-uuid');
|
|
2848
2334
|
* ```
|
|
2849
2335
|
*/
|
|
2850
|
-
async
|
|
2851
|
-
return await this.userService.
|
|
2336
|
+
async getUserForAuthContext(sub) {
|
|
2337
|
+
return await this.userService.getUserForAuthContext(sub);
|
|
2852
2338
|
}
|
|
2853
2339
|
/**
|
|
2854
2340
|
* Validate JWT access token
|
|
@@ -2886,7 +2372,6 @@ class AuthService {
|
|
|
2886
2372
|
* ```
|
|
2887
2373
|
*/
|
|
2888
2374
|
async validateAccessToken(dto) {
|
|
2889
|
-
// Ensure DTO is validated (supports direct usage without framework validation)
|
|
2890
2375
|
dto = await (0, dto_validator_1.ensureValidatedDto)(validate_access_token_dto_1.ValidateAccessTokenDTO, dto);
|
|
2891
2376
|
const result = await this.jwtService.validateAccessToken(dto.accessToken);
|
|
2892
2377
|
return {
|
|
@@ -2897,368 +2382,6 @@ class AuthService {
|
|
|
2897
2382
|
};
|
|
2898
2383
|
}
|
|
2899
2384
|
// ============================================================================
|
|
2900
|
-
// Helper Methods
|
|
2901
|
-
// ============================================================================
|
|
2902
|
-
// NOTE: Private helper methods have been moved to AuthServiceInternalHelpers
|
|
2903
|
-
// Use this.helpers.methodName() to access them
|
|
2904
|
-
/**
|
|
2905
|
-
* Get user for authentication context
|
|
2906
|
-
*
|
|
2907
|
-
* Loads user by sub (external identifier) with all fields needed for auth context.
|
|
2908
|
-
* Computes hasPasswordHash from passwordHash, then removes passwordHash and other sensitive fields.
|
|
2909
|
-
*
|
|
2910
|
-
* This method is used by AuthHandler and AuthGuard to load authenticated users.
|
|
2911
|
-
* It ensures consistent user object shape across platforms (core + NestJS).
|
|
2912
|
-
*
|
|
2913
|
-
* @param sub - External user identifier (UUID)
|
|
2914
|
-
* @returns User object with hasPasswordHash flag, without sensitive fields
|
|
2915
|
-
* @throws {NAuthException} If user not found or account is inactive
|
|
2916
|
-
*
|
|
2917
|
-
* @example
|
|
2918
|
-
* ```typescript
|
|
2919
|
-
* const user = await authService.getUserForAuthContext('user-uuid-123');
|
|
2920
|
-
* // user.hasPasswordHash === true/false
|
|
2921
|
-
* // user.passwordHash === undefined (removed)
|
|
2922
|
-
* ```
|
|
2923
|
-
*/
|
|
2924
|
-
async getUserForAuthContext(sub) {
|
|
2925
|
-
return await this.userService.getUserForAuthContext(sub);
|
|
2926
|
-
}
|
|
2927
|
-
/**
|
|
2928
|
-
* Get user by external identifier (sub/UUID).
|
|
2929
|
-
*
|
|
2930
|
-
* @param dto - GetUserByIdDTO containing sub
|
|
2931
|
-
* @returns User response DTO or null if not found
|
|
2932
|
-
*
|
|
2933
|
-
* @example
|
|
2934
|
-
* ```typescript
|
|
2935
|
-
* const user = await authService.getUserById({ sub: 'user-uuid' });
|
|
2936
|
-
* ```
|
|
2937
|
-
*/
|
|
2938
|
-
async getUserById(dto) {
|
|
2939
|
-
return await this.userService.getUserById(dto);
|
|
2940
|
-
}
|
|
2941
|
-
/**
|
|
2942
|
-
* Get user by email address.
|
|
2943
|
-
*
|
|
2944
|
-
* @param dto - GetUserByEmailDTO containing email and optional requireEmailVerified
|
|
2945
|
-
* @returns User response DTO or null if not found
|
|
2946
|
-
* @internal - For use by social auth providers
|
|
2947
|
-
*
|
|
2948
|
-
* @example
|
|
2949
|
-
* ```typescript
|
|
2950
|
-
* const user = await authService.getUserByEmail({ email: 'user@example.com', requireEmailVerified: true });
|
|
2951
|
-
* ```
|
|
2952
|
-
*/
|
|
2953
|
-
async getUserByEmail(dto) {
|
|
2954
|
-
return await this.userService.getUserByEmail(dto);
|
|
2955
|
-
}
|
|
2956
|
-
/**
|
|
2957
|
-
* Require user to change password at next login.
|
|
2958
|
-
*
|
|
2959
|
-
* Throws if user not found or has no password set (e.g. social login only).
|
|
2960
|
-
*
|
|
2961
|
-
* @param dto - SetMustChangePasswordDTO containing userId (sub)
|
|
2962
|
-
* @returns Success response
|
|
2963
|
-
* @throws {NAuthException} If user is not found or cannot change password
|
|
2964
|
-
*
|
|
2965
|
-
* @example
|
|
2966
|
-
* ```typescript
|
|
2967
|
-
* await authService.setMustChangePassword({ userId: 'user-uuid-123' });
|
|
2968
|
-
* ```
|
|
2969
|
-
*/
|
|
2970
|
-
async setMustChangePassword(dto) {
|
|
2971
|
-
return await this.userService.setMustChangePassword(dto);
|
|
2972
|
-
}
|
|
2973
|
-
/**
|
|
2974
|
-
* Admin-only: Initiate a code-based password reset workflow.
|
|
2975
|
-
*
|
|
2976
|
-
* Unlike adminSetPassword(), this sends a verification code (and optional link)
|
|
2977
|
-
* to the user via email/SMS and allows them to set their own password.
|
|
2978
|
-
*
|
|
2979
|
-
* Features:
|
|
2980
|
-
* - Code + optional link delivery (like email verification)
|
|
2981
|
-
* - Optional immediate session revocation
|
|
2982
|
-
* - Configurable expiry (default 1 hour)
|
|
2983
|
-
* - Admin-specific email template
|
|
2984
|
-
* - No rate limiting (admin bypass)
|
|
2985
|
-
* - Separate audit trail with reason
|
|
2986
|
-
*
|
|
2987
|
-
* Security:
|
|
2988
|
-
* - Admin-only operation (protect route with admin guard)
|
|
2989
|
-
* - Non-enumerating (throws NOT_FOUND if user doesn't exist)
|
|
2990
|
-
* - Separate token type ('admin_password_reset')
|
|
2991
|
-
* - Audit logging with reason
|
|
2992
|
-
*
|
|
2993
|
-
* @param dto - Admin reset password request
|
|
2994
|
-
* @returns Response with masked destination, expiry, and sessions revoked count
|
|
2995
|
-
* @throws {NAuthException} NOT_FOUND when user not found
|
|
2996
|
-
*
|
|
2997
|
-
* @example
|
|
2998
|
-
* ```typescript
|
|
2999
|
-
* // With link for custom UI
|
|
3000
|
-
* const result = await authService.adminResetPassword({
|
|
3001
|
-
* identifier: 'user@example.com',
|
|
3002
|
-
* baseUrl: 'https://myapp.com/reset-password',
|
|
3003
|
-
* revokeSessions: true,
|
|
3004
|
-
* reason: 'User reported compromise'
|
|
3005
|
-
* });
|
|
3006
|
-
* // result: { success: true, destination: 'u***r@example.com', expiresIn: 3600, sessionsRevoked: 3 }
|
|
3007
|
-
*
|
|
3008
|
-
* // Code only (no link)
|
|
3009
|
-
* const result = await authService.adminResetPassword({
|
|
3010
|
-
* identifier: 'user@example.com'
|
|
3011
|
-
* });
|
|
3012
|
-
* ```
|
|
3013
|
-
*/
|
|
3014
|
-
async adminResetPassword(dto) {
|
|
3015
|
-
// Ensure DTO is validated (supports direct usage without framework validation)
|
|
3016
|
-
dto = await (0, dto_validator_1.ensureValidatedDto)(admin_reset_password_dto_1.AdminResetPasswordDTO, dto);
|
|
3017
|
-
this.logger?.log?.(`Admin password reset requested for identifier: ${dto.identifier}`);
|
|
3018
|
-
this.logger?.debug?.(`Reset details: { identifier: ${dto.identifier}, deliveryMethod: ${dto.deliveryMethod ?? 'email'}, revokeSessions: ${dto.revokeSessions ?? false}, baseUrl: ${dto.baseUrl ?? 'none'}, reason: ${dto.reason ?? 'none'} }`);
|
|
3019
|
-
// ============================================================================
|
|
3020
|
-
// Find User by Identifier
|
|
3021
|
-
// ============================================================================
|
|
3022
|
-
// Support multiple identifier types: email, username, phone, or sub (UUID)
|
|
3023
|
-
let user = null;
|
|
3024
|
-
// Try to find by sub (UUID) first if it looks like a UUID.
|
|
3025
|
-
// WHY: Many deployments treat `sub` as the primary immutable identifier.
|
|
3026
|
-
if ((0, class_validator_1.isUUID)(dto.identifier)) {
|
|
3027
|
-
this.logger?.debug?.(`Identifier appears to be UUID, searching by sub: ${dto.identifier}`);
|
|
3028
|
-
user = (await this.userRepository.findOne({ where: { sub: dto.identifier } }));
|
|
3029
|
-
}
|
|
3030
|
-
// If not found by sub, try by identifier (email, username, phone)
|
|
3031
|
-
if (!user) {
|
|
3032
|
-
this.logger?.debug?.(`Searching by identifier (email/username/phone): ${dto.identifier}`);
|
|
3033
|
-
user = await this.helpers.findUserByIdentifier(dto.identifier);
|
|
3034
|
-
}
|
|
3035
|
-
if (!user) {
|
|
3036
|
-
this.logger?.warn?.(`Admin password reset failed - user not found: ${dto.identifier}`);
|
|
3037
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
3038
|
-
}
|
|
3039
|
-
if (!this.passwordResetService) {
|
|
3040
|
-
this.logger?.error?.('Password reset service not available');
|
|
3041
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SERVICE_UNAVAILABLE, 'Password reset service is not configured. Please configure an email provider.');
|
|
3042
|
-
}
|
|
3043
|
-
// ============================================================================
|
|
3044
|
-
// Optionally revoke sessions immediately (before sending reset email)
|
|
3045
|
-
// ============================================================================
|
|
3046
|
-
const revokeSessions = dto.revokeSessions ?? false;
|
|
3047
|
-
let sessionsRevoked = 0;
|
|
3048
|
-
if (revokeSessions) {
|
|
3049
|
-
sessionsRevoked = await this.sessionService.revokeAllUserSessions(user.id, 'Admin initiated password reset');
|
|
3050
|
-
this.logger?.log?.(`Revoked ${sessionsRevoked} sessions for user ${user.sub}`);
|
|
3051
|
-
}
|
|
3052
|
-
// ============================================================================
|
|
3053
|
-
// Request admin reset with code + link
|
|
3054
|
-
// ============================================================================
|
|
3055
|
-
const delivery = dto.deliveryMethod || 'email';
|
|
3056
|
-
const expiresIn = dto.codeExpiresIn || 3600; // Default 1 hour
|
|
3057
|
-
const result = await this.passwordResetService.requestAdminReset(user, delivery, {
|
|
3058
|
-
expiresIn,
|
|
3059
|
-
baseUrl: dto.baseUrl, // Consumer app can build custom UI
|
|
3060
|
-
});
|
|
3061
|
-
// ============================================================================
|
|
3062
|
-
// Audit Logging
|
|
3063
|
-
// ============================================================================
|
|
3064
|
-
await this.auditService?.recordEvent({
|
|
3065
|
-
userId: user.id,
|
|
3066
|
-
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ADMIN_PASSWORD_RESET_INITIATED,
|
|
3067
|
-
eventStatus: 'INFO',
|
|
3068
|
-
authMethod: 'password',
|
|
3069
|
-
description: dto.reason || 'Admin initiated password reset',
|
|
3070
|
-
reason: dto.reason, // Store reason in audit event
|
|
3071
|
-
metadata: {
|
|
3072
|
-
medium: delivery,
|
|
3073
|
-
expiresIn,
|
|
3074
|
-
sessionsRevoked,
|
|
3075
|
-
hasBaseUrl: !!dto.baseUrl,
|
|
3076
|
-
},
|
|
3077
|
-
});
|
|
3078
|
-
// ============================================================================
|
|
3079
|
-
// Return Response
|
|
3080
|
-
// ============================================================================
|
|
3081
|
-
return {
|
|
3082
|
-
success: true,
|
|
3083
|
-
destination: result.destination,
|
|
3084
|
-
deliveryMedium: result.deliveryMedium,
|
|
3085
|
-
expiresIn: result.expiresIn,
|
|
3086
|
-
sessionsRevoked: revokeSessions ? sessionsRevoked : undefined,
|
|
3087
|
-
};
|
|
3088
|
-
}
|
|
3089
|
-
/**
|
|
3090
|
-
* Complete admin-initiated password reset with a verification code.
|
|
3091
|
-
*
|
|
3092
|
-
* NOTE:
|
|
3093
|
-
* - Links (when provided) should include the same verification code as a query parameter
|
|
3094
|
-
* (e.g., `...?code=123456`) to keep consumer apps code-only and consistent.
|
|
3095
|
-
*
|
|
3096
|
-
* Security:
|
|
3097
|
-
* - Verifies code via PasswordResetService
|
|
3098
|
-
* - Enforces password policy and history
|
|
3099
|
-
* - Always revokes all sessions on completion
|
|
3100
|
-
* - Does not force password change (user already set new password)
|
|
3101
|
-
* - Records audit event
|
|
3102
|
-
*
|
|
3103
|
-
* @param dto - Confirm admin reset password request
|
|
3104
|
-
* @returns Success response
|
|
3105
|
-
* @throws {NAuthException} NOT_FOUND | PASSWORD_RESET_CODE_INVALID | PASSWORD_RESET_CODE_EXPIRED | PASSWORD_RESET_MAX_ATTEMPTS | WEAK_PASSWORD | PASSWORD_REUSED | INVALID_CREDENTIALS
|
|
3106
|
-
*
|
|
3107
|
-
* @example
|
|
3108
|
-
* ```typescript
|
|
3109
|
-
* await authService.confirmAdminResetPassword({
|
|
3110
|
-
* identifier: 'user@example.com',
|
|
3111
|
-
* code: '123456',
|
|
3112
|
-
* newPassword: 'NewSecurePass123!'
|
|
3113
|
-
* });
|
|
3114
|
-
* ```
|
|
3115
|
-
*/
|
|
3116
|
-
async confirmAdminResetPassword(dto) {
|
|
3117
|
-
// Ensure DTO is validated (supports direct usage without framework validation)
|
|
3118
|
-
dto = await (0, dto_validator_1.ensureValidatedDto)(admin_reset_password_dto_1.ConfirmAdminResetPasswordDTO, dto);
|
|
3119
|
-
this.logger?.log?.(`Confirm admin password reset for identifier: ${dto.identifier}`);
|
|
3120
|
-
// ============================================================================
|
|
3121
|
-
// Find User by Identifier
|
|
3122
|
-
// ============================================================================
|
|
3123
|
-
let user = null;
|
|
3124
|
-
if ((0, class_validator_1.isUUID)(dto.identifier)) {
|
|
3125
|
-
this.logger?.debug?.(`Identifier appears to be UUID, searching by sub: ${dto.identifier}`);
|
|
3126
|
-
user = (await this.userRepository.findOne({ where: { sub: dto.identifier } }));
|
|
3127
|
-
}
|
|
3128
|
-
if (!user) {
|
|
3129
|
-
this.logger?.debug?.(`Searching by identifier (email/username/phone): ${dto.identifier}`);
|
|
3130
|
-
user = await this.helpers.findUserByIdentifier(dto.identifier);
|
|
3131
|
-
}
|
|
3132
|
-
if (!user) {
|
|
3133
|
-
this.logger?.warn?.(`Confirm admin reset failed - user not found: ${dto.identifier}`);
|
|
3134
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
3135
|
-
}
|
|
3136
|
-
if (!this.passwordResetService) {
|
|
3137
|
-
this.logger?.error?.('Password reset service not available');
|
|
3138
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SERVICE_UNAVAILABLE, 'Password reset service is not configured. Please configure an email provider.');
|
|
3139
|
-
}
|
|
3140
|
-
// ============================================================================
|
|
3141
|
-
// Verify code
|
|
3142
|
-
// ============================================================================
|
|
3143
|
-
await this.passwordResetService.consumeValidCode(user, dto.code, 'admin_password_reset');
|
|
3144
|
-
// ============================================================================
|
|
3145
|
-
// Update password
|
|
3146
|
-
// ============================================================================
|
|
3147
|
-
// WHY: User already set a new password via this reset flow, so no need to force
|
|
3148
|
-
// another password change on next login (unlike adminSetPassword where admin sets
|
|
3149
|
-
// a password the user doesn't know)
|
|
3150
|
-
await this.helpers.updateUserPassword({
|
|
3151
|
-
user,
|
|
3152
|
-
newPassword: dto.newPassword,
|
|
3153
|
-
mustChangePassword: false, // User already set new password, no need to force change again
|
|
3154
|
-
revokeSessions: true, // Always revoke on completion
|
|
3155
|
-
revokeReason: 'Admin-initiated password reset completed',
|
|
3156
|
-
audit: {
|
|
3157
|
-
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ADMIN_PASSWORD_RESET_COMPLETED,
|
|
3158
|
-
eventStatus: 'SUCCESS',
|
|
3159
|
-
description: 'User completed admin-initiated password reset',
|
|
3160
|
-
metadata: {
|
|
3161
|
-
usedCode: true,
|
|
3162
|
-
},
|
|
3163
|
-
},
|
|
3164
|
-
}, this.passwordService, this.auditService);
|
|
3165
|
-
// ============================================================================
|
|
3166
|
-
// Return Response
|
|
3167
|
-
// ============================================================================
|
|
3168
|
-
return {
|
|
3169
|
-
success: true,
|
|
3170
|
-
};
|
|
3171
|
-
}
|
|
3172
|
-
/**
|
|
3173
|
-
* Admin-only: Reset a user's password by identifier.
|
|
3174
|
-
*
|
|
3175
|
-
* Allows administrators to reset a user's password using any identifier
|
|
3176
|
-
* (email, username, phone, or sub). Automatically revokes sessions and optionally
|
|
3177
|
-
* requires password change on next login using the existing challenge system.
|
|
3178
|
-
*
|
|
3179
|
-
* SECURITY: This is an admin-only operation. Ensure proper authorization
|
|
3180
|
-
* checks are in place before calling this method.
|
|
3181
|
-
*
|
|
3182
|
-
* @param dto - Admin reset password request
|
|
3183
|
-
* @returns Response with success status and session revocation count
|
|
3184
|
-
* @throws {NAuthException} If user not found, user has no password (social-only), or password validation fails
|
|
3185
|
-
*
|
|
3186
|
-
* @example
|
|
3187
|
-
* ```typescript
|
|
3188
|
-
* // Reset with force password change
|
|
3189
|
-
* const result = await authService.adminSetPassword({
|
|
3190
|
-
* identifier: 'user@example.com',
|
|
3191
|
-
* newPassword: 'NewSecurePassword123!',
|
|
3192
|
-
* mustChangePassword: true,
|
|
3193
|
-
* revokeSessions: true
|
|
3194
|
-
* });
|
|
3195
|
-
*
|
|
3196
|
-
* // Reset without forcing password change
|
|
3197
|
-
* const result = await authService.adminSetPassword({
|
|
3198
|
-
* identifier: 'a21b654c-2746-4168-acee-c175083a65cd',
|
|
3199
|
-
* newPassword: 'NewSecurePassword123!',
|
|
3200
|
-
* mustChangePassword: false
|
|
3201
|
-
* });
|
|
3202
|
-
* ```
|
|
3203
|
-
*/
|
|
3204
|
-
async adminSetPassword(dto) {
|
|
3205
|
-
// Ensure DTO is validated (supports direct usage without framework validation)
|
|
3206
|
-
dto = await (0, dto_validator_1.ensureValidatedDto)(admin_set_password_dto_1.AdminSetPasswordDTO, dto);
|
|
3207
|
-
this.logger?.log?.(`Admin password reset requested for identifier: ${dto.identifier}`);
|
|
3208
|
-
this.logger?.debug?.(`Reset details: { identifier: ${dto.identifier}, mustChangePassword: ${dto.mustChangePassword ?? true}, revokeSessions: ${dto.revokeSessions ?? true} }`);
|
|
3209
|
-
// ============================================================================
|
|
3210
|
-
// Find User by Identifier
|
|
3211
|
-
// ============================================================================
|
|
3212
|
-
// Support multiple identifier types: email, username, phone, or sub (UUID)
|
|
3213
|
-
let user = null;
|
|
3214
|
-
// Try to find by sub (UUID) first if it looks like a UUID.
|
|
3215
|
-
// WHY: Many deployments treat `sub` as the primary immutable identifier.
|
|
3216
|
-
if ((0, class_validator_1.isUUID)(dto.identifier)) {
|
|
3217
|
-
this.logger?.debug?.(`Identifier appears to be UUID, searching by sub: ${dto.identifier}`);
|
|
3218
|
-
user = (await this.userRepository.findOne({ where: { sub: dto.identifier } }));
|
|
3219
|
-
}
|
|
3220
|
-
// If not found by sub, try by identifier (email, username, phone)
|
|
3221
|
-
if (!user) {
|
|
3222
|
-
this.logger?.debug?.(`Searching by identifier (email/username/phone): ${dto.identifier}`);
|
|
3223
|
-
user = await this.helpers.findUserByIdentifier(dto.identifier);
|
|
3224
|
-
}
|
|
3225
|
-
if (!user) {
|
|
3226
|
-
this.logger?.warn?.(`Password reset failed - user not found: ${dto.identifier}`);
|
|
3227
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
3228
|
-
}
|
|
3229
|
-
const mustChangePassword = dto.mustChangePassword ?? true; // Default to true for security
|
|
3230
|
-
const revokeSessions = dto.revokeSessions !== false;
|
|
3231
|
-
const wasSocialOnly = !user.passwordHash;
|
|
3232
|
-
const { sessionsRevoked } = await this.helpers.updateUserPassword({
|
|
3233
|
-
user,
|
|
3234
|
-
newPassword: dto.newPassword,
|
|
3235
|
-
mustChangePassword,
|
|
3236
|
-
revokeSessions,
|
|
3237
|
-
revokeReason: 'Password reset by administrator',
|
|
3238
|
-
audit: {
|
|
3239
|
-
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.PASSWORD_RESET_COMPLETED,
|
|
3240
|
-
eventStatus: 'SUCCESS',
|
|
3241
|
-
reason: 'admin_reset',
|
|
3242
|
-
description: 'Password reset by administrator',
|
|
3243
|
-
metadata: {
|
|
3244
|
-
identifier: dto.identifier,
|
|
3245
|
-
mustChangePassword,
|
|
3246
|
-
// WHY: Admins can set the first password for social-only accounts so users can login via either route later.
|
|
3247
|
-
// This flag helps downstream observability without exposing anything to clients.
|
|
3248
|
-
wasSocialOnly,
|
|
3249
|
-
},
|
|
3250
|
-
},
|
|
3251
|
-
}, this.passwordService, this.auditService);
|
|
3252
|
-
// ============================================================================
|
|
3253
|
-
// Return Response
|
|
3254
|
-
// ============================================================================
|
|
3255
|
-
return {
|
|
3256
|
-
success: true,
|
|
3257
|
-
mustChangePassword,
|
|
3258
|
-
sessionsRevoked,
|
|
3259
|
-
};
|
|
3260
|
-
}
|
|
3261
|
-
// ============================================================================
|
|
3262
2385
|
// Forgot Password (Account Recovery)
|
|
3263
2386
|
// ============================================================================
|
|
3264
2387
|
/**
|
|
@@ -3394,6 +2517,16 @@ class AuthService {
|
|
|
3394
2517
|
}, this.passwordService, this.auditService);
|
|
3395
2518
|
return { success: true, mustChangePassword: false };
|
|
3396
2519
|
}
|
|
2520
|
+
// ============================================================================
|
|
2521
|
+
// Helper Methods
|
|
2522
|
+
// ============================================================================
|
|
2523
|
+
getCurrentUserOrThrow() {
|
|
2524
|
+
const currentUser = context_storage_1.ContextStorage.get('CURRENT_USER');
|
|
2525
|
+
if (!currentUser) {
|
|
2526
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.FORBIDDEN, 'Authentication required');
|
|
2527
|
+
}
|
|
2528
|
+
return currentUser;
|
|
2529
|
+
}
|
|
3397
2530
|
}
|
|
3398
2531
|
exports.AuthService = AuthService;
|
|
3399
2532
|
//# sourceMappingURL=auth.service.js.map
|