@nauth-toolkit/core 0.1.18 → 0.1.22
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/adapters/storage.factory.d.ts.map +1 -1
- package/dist/adapters/storage.factory.js +250 -18
- package/dist/adapters/storage.factory.js.map +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +3 -2
- package/dist/bootstrap.js.map +1 -1
- package/dist/dto/admin-signup.dto.d.ts +196 -0
- package/dist/dto/admin-signup.dto.d.ts.map +1 -0
- package/dist/dto/admin-signup.dto.js +317 -0
- package/dist/dto/admin-signup.dto.js.map +1 -0
- package/dist/dto/auth-response.dto.d.ts +14 -0
- package/dist/dto/auth-response.dto.d.ts.map +1 -1
- package/dist/dto/auth-response.dto.js +14 -0
- package/dist/dto/auth-response.dto.js.map +1 -1
- package/dist/dto/index.d.ts +1 -0
- package/dist/dto/index.d.ts.map +1 -1
- package/dist/dto/index.js +1 -0
- package/dist/dto/index.js.map +1 -1
- package/dist/dto/social-auth.dto.d.ts +24 -0
- package/dist/dto/social-auth.dto.d.ts.map +1 -1
- package/dist/dto/social-auth.dto.js +37 -1
- package/dist/dto/social-auth.dto.js.map +1 -1
- package/dist/entities/user.entity.d.ts +8 -0
- package/dist/entities/user.entity.d.ts.map +1 -1
- package/dist/entities/user.entity.js +8 -0
- package/dist/entities/user.entity.js.map +1 -1
- package/dist/handlers/auth.handler.d.ts +3 -8
- package/dist/handlers/auth.handler.d.ts.map +1 -1
- package/dist/handlers/auth.handler.js +10 -55
- package/dist/handlers/auth.handler.js.map +1 -1
- package/dist/handlers/csrf.handler.d.ts.map +1 -1
- package/dist/handlers/csrf.handler.js +7 -2
- package/dist/handlers/csrf.handler.js.map +1 -1
- package/dist/handlers/social-redirect.handler.d.ts +136 -0
- package/dist/handlers/social-redirect.handler.d.ts.map +1 -0
- package/dist/handlers/social-redirect.handler.js +364 -0
- package/dist/handlers/social-redirect.handler.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/config.interface.d.ts +43 -0
- package/dist/interfaces/config.interface.d.ts.map +1 -1
- package/dist/interfaces/entities.interface.d.ts +6 -0
- package/dist/interfaces/entities.interface.d.ts.map +1 -1
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/interfaces/index.d.ts.map +1 -1
- package/dist/interfaces/index.js +1 -0
- package/dist/interfaces/index.js.map +1 -1
- package/dist/interfaces/social-auth-state-store.interface.d.ts +100 -0
- package/dist/interfaces/social-auth-state-store.interface.d.ts.map +1 -0
- package/dist/interfaces/social-auth-state-store.interface.js +3 -0
- package/dist/interfaces/social-auth-state-store.interface.js.map +1 -0
- package/dist/interfaces/storage-adapter.interface.d.ts +2 -2
- package/dist/interfaces/storage-adapter.interface.d.ts.map +1 -1
- package/dist/internal.d.ts +5 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +7 -1
- package/dist/internal.js.map +1 -1
- package/dist/schemas/auth-config.schema.d.ts +107 -28
- package/dist/schemas/auth-config.schema.d.ts.map +1 -1
- package/dist/schemas/auth-config.schema.js +20 -1
- package/dist/schemas/auth-config.schema.js.map +1 -1
- package/dist/services/auth-challenge-helper.service.d.ts +1 -1
- package/dist/services/auth-challenge-helper.service.d.ts.map +1 -1
- package/dist/services/auth-challenge-helper.service.js +9 -4
- package/dist/services/auth-challenge-helper.service.js.map +1 -1
- package/dist/services/auth.service.d.ts +59 -3
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +276 -50
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/geo-location.service.js +2 -2
- package/dist/services/geo-location.service.js.map +1 -1
- package/dist/services/password-reset.service.d.ts.map +1 -1
- package/dist/services/password-reset.service.js.map +1 -1
- package/dist/services/phone-verification.service.js.map +1 -1
- package/dist/services/social-auth-base.service.d.ts +5 -10
- package/dist/services/social-auth-base.service.d.ts.map +1 -1
- package/dist/services/social-auth-base.service.js +30 -61
- package/dist/services/social-auth-base.service.js.map +1 -1
- package/dist/services/social-auth-state-store.service.d.ts +58 -0
- package/dist/services/social-auth-state-store.service.d.ts.map +1 -0
- package/dist/services/social-auth-state-store.service.js +261 -0
- package/dist/services/social-auth-state-store.service.js.map +1 -0
- package/dist/storage/account-lockout-storage.service.d.ts +2 -2
- package/dist/storage/account-lockout-storage.service.d.ts.map +1 -1
- package/dist/storage/account-lockout-storage.service.js +2 -2
- package/dist/storage/account-lockout-storage.service.js.map +1 -1
- package/dist/templates/sms-template.engine.d.ts.map +1 -1
- package/dist/templates/sms-template.engine.js +1 -2
- package/dist/templates/sms-template.engine.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/password-generator.d.ts +29 -0
- package/dist/utils/password-generator.d.ts.map +1 -0
- package/dist/utils/password-generator.js +98 -0
- package/dist/utils/password-generator.js.map +1 -0
- package/dist/utils/setup/init-social.d.ts +2 -5
- package/dist/utils/setup/init-social.d.ts.map +1 -1
- package/dist/utils/setup/init-social.js.map +1 -1
- package/package.json +1 -1
|
@@ -47,6 +47,7 @@ const error_codes_enum_1 = require("../enums/error-codes.enum");
|
|
|
47
47
|
const mfa_method_enum_1 = require("../enums/mfa-method.enum");
|
|
48
48
|
const class_validator_1 = require("class-validator");
|
|
49
49
|
const crypto = __importStar(require("crypto"));
|
|
50
|
+
const password_generator_1 = require("../utils/password-generator");
|
|
50
51
|
/**
|
|
51
52
|
* Dummy Argon2 hash for constant-time response
|
|
52
53
|
*
|
|
@@ -289,6 +290,199 @@ class AuthService {
|
|
|
289
290
|
return response;
|
|
290
291
|
}
|
|
291
292
|
// ============================================================================
|
|
293
|
+
// Admin Signup
|
|
294
|
+
// ============================================================================
|
|
295
|
+
/**
|
|
296
|
+
* Administrative user creation with override capabilities
|
|
297
|
+
*
|
|
298
|
+
* Allows administrators to create user accounts with:
|
|
299
|
+
* - Bypass email/phone verification requirements
|
|
300
|
+
* - Force password change on first login
|
|
301
|
+
* - Auto-generate secure passwords
|
|
302
|
+
*
|
|
303
|
+
* Security:
|
|
304
|
+
* - No built-in authentication - endpoint must be protected by framework adapter
|
|
305
|
+
* - All duplicate checks still enforced
|
|
306
|
+
* - Password policy still enforced (unless auto-generated)
|
|
307
|
+
* - Audit trail records admin-created accounts
|
|
308
|
+
*
|
|
309
|
+
* @param dto - Admin signup DTO with override flags
|
|
310
|
+
* @returns User object and optionally generated password
|
|
311
|
+
* @throws {NAuthException} EMAIL_EXISTS | USERNAME_EXISTS | PHONE_EXISTS | WEAK_PASSWORD
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* // Create user with pre-verified email
|
|
316
|
+
* const result = await authService.adminSignup({
|
|
317
|
+
* email: 'user@example.com',
|
|
318
|
+
* password: 'SecurePass123!',
|
|
319
|
+
* isEmailVerified: true,
|
|
320
|
+
* });
|
|
321
|
+
*
|
|
322
|
+
* // Create user with auto-generated password
|
|
323
|
+
* const result = await authService.adminSignup({
|
|
324
|
+
* email: 'user@example.com',
|
|
325
|
+
* generatePassword: true,
|
|
326
|
+
* isEmailVerified: true,
|
|
327
|
+
* mustChangePassword: true,
|
|
328
|
+
* });
|
|
329
|
+
* // result.generatedPassword contains the temporary password
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
async adminSignup(dto) {
|
|
333
|
+
// Get client info from request context (transparent!)
|
|
334
|
+
const clientInfo = this.clientInfoService.get();
|
|
335
|
+
this.logger?.log?.(`Admin signup attempt for email: ${dto.email}`);
|
|
336
|
+
this.logger?.debug?.(`Admin signup details: { email: ${dto.email}, username: ${dto.username || 'none'}, ip: ${clientInfo.ipAddress} }`);
|
|
337
|
+
// Skip signup.enabled check (admin bypass)
|
|
338
|
+
// Check if user already exists (email and username)
|
|
339
|
+
this.logger?.debug?.(`Checking if user exists: ${dto.email}`);
|
|
340
|
+
const existingUserByEmail = await this.userRepository.findOne({
|
|
341
|
+
where: { email: dto.email },
|
|
342
|
+
});
|
|
343
|
+
if (existingUserByEmail) {
|
|
344
|
+
this.logger?.warn?.(`Admin signup failed - user already exists: ${dto.email}`);
|
|
345
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
|
|
346
|
+
}
|
|
347
|
+
// Check for duplicate username if provided
|
|
348
|
+
if (dto.username) {
|
|
349
|
+
this.logger?.debug?.(`Checking if username exists: ${dto.username}`);
|
|
350
|
+
const existingUserByUsername = await this.userRepository.findOne({
|
|
351
|
+
where: { username: dto.username },
|
|
352
|
+
});
|
|
353
|
+
if (existingUserByUsername) {
|
|
354
|
+
this.logger?.warn?.(`Admin signup failed - username already exists: ${dto.username}`);
|
|
355
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Check for duplicate phone if provided and duplicates not allowed
|
|
359
|
+
if (dto.phone && !this.config.signup?.allowDuplicatePhones) {
|
|
360
|
+
this.logger?.debug?.(`Checking if phone exists: ${dto.phone}`);
|
|
361
|
+
const existingUserByPhone = await this.userRepository.findOne({
|
|
362
|
+
where: { phone: dto.phone },
|
|
363
|
+
});
|
|
364
|
+
if (existingUserByPhone) {
|
|
365
|
+
this.logger?.warn?.(`Admin signup failed - phone already exists: ${dto.phone}`);
|
|
366
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Handle password
|
|
370
|
+
let passwordHash;
|
|
371
|
+
let generatedPassword;
|
|
372
|
+
if (dto.generatePassword) {
|
|
373
|
+
// Generate secure random password
|
|
374
|
+
generatedPassword = (0, password_generator_1.generateSecurePassword)(16);
|
|
375
|
+
this.logger?.debug?.(`Generated password for admin-created user: ${dto.email}`);
|
|
376
|
+
passwordHash = await this.passwordService.hashPassword(generatedPassword);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// Validate password policy
|
|
380
|
+
if (!dto.password) {
|
|
381
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.WEAK_PASSWORD, 'Password is required when generatePassword is false');
|
|
382
|
+
}
|
|
383
|
+
this.logger?.debug?.('Validating password against policy');
|
|
384
|
+
const passwordValidation = await this.passwordService.validatePassword(dto.password, {
|
|
385
|
+
email: dto.email,
|
|
386
|
+
username: dto.username,
|
|
387
|
+
});
|
|
388
|
+
if (!passwordValidation.valid) {
|
|
389
|
+
this.logger?.warn?.(`Password validation failed for ${dto.email}: ${passwordValidation.errors.join(', ')}`);
|
|
390
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.WEAK_PASSWORD, passwordValidation.errors.join(', '), {
|
|
391
|
+
errors: passwordValidation.errors,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// Hash password
|
|
395
|
+
passwordHash = await this.passwordService.hashPassword(dto.password);
|
|
396
|
+
}
|
|
397
|
+
// Create user with override flags
|
|
398
|
+
this.logger?.debug?.(`Creating admin user record for: ${dto.email} || ${dto.username} || ${dto.phone} (isEmailVerified: ${dto.isEmailVerified || false}, isPhoneVerified: ${dto.isPhoneVerified || false})`);
|
|
399
|
+
const user = this.userRepository.create({
|
|
400
|
+
email: dto.email,
|
|
401
|
+
username: dto.username,
|
|
402
|
+
firstName: dto.firstName,
|
|
403
|
+
lastName: dto.lastName,
|
|
404
|
+
phone: dto.phone,
|
|
405
|
+
passwordHash,
|
|
406
|
+
passwordChangedAt: new Date(),
|
|
407
|
+
isEmailVerified: dto.isEmailVerified ?? false, // Use DTO value or default to false
|
|
408
|
+
isPhoneVerified: dto.isPhoneVerified ?? false, // Use DTO value or default to false
|
|
409
|
+
mustChangePassword: dto.mustChangePassword ?? false, // Use DTO value or default to false
|
|
410
|
+
isActive: true, // Always active
|
|
411
|
+
metadata: dto.metadata,
|
|
412
|
+
});
|
|
413
|
+
let savedUser;
|
|
414
|
+
try {
|
|
415
|
+
savedUser = (await this.userRepository.save(user));
|
|
416
|
+
this.logger?.log?.(`Admin user created successfully: ${dto.email} (sub: ${savedUser.sub})`);
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// Audit: Record account creation by admin
|
|
419
|
+
// ============================================================================
|
|
420
|
+
try {
|
|
421
|
+
await this.auditService?.recordEvent({
|
|
422
|
+
userId: savedUser.id,
|
|
423
|
+
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.ACCOUNT_CREATED,
|
|
424
|
+
eventStatus: 'INFO',
|
|
425
|
+
authMethod: 'admin',
|
|
426
|
+
// Client info automatically included from context
|
|
427
|
+
metadata: {
|
|
428
|
+
email: savedUser.email,
|
|
429
|
+
username: savedUser.username || null,
|
|
430
|
+
createdByAdmin: true,
|
|
431
|
+
adminIdentifier: clientInfo.ipAddress || 'unknown',
|
|
432
|
+
isEmailVerified: savedUser.isEmailVerified,
|
|
433
|
+
isPhoneVerified: savedUser.isPhoneVerified,
|
|
434
|
+
mustChangePassword: savedUser.mustChangePassword,
|
|
435
|
+
passwordGenerated: !!generatedPassword,
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
catch (auditError) {
|
|
440
|
+
// Non-blocking: Log but continue
|
|
441
|
+
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
442
|
+
this.logger?.error?.(`Failed to record ACCOUNT_CREATED audit event: ${errorMessage}`, {
|
|
443
|
+
error: auditError,
|
|
444
|
+
userId: savedUser.id,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
// Handle database constraint violations gracefully
|
|
450
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === '23505') {
|
|
451
|
+
// PostgreSQL unique constraint violation
|
|
452
|
+
const dbError = error;
|
|
453
|
+
if (dbError.detail?.includes('email')) {
|
|
454
|
+
this.logger?.warn?.(`Admin signup failed - email constraint violation: ${dto.email}`);
|
|
455
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this email already exists');
|
|
456
|
+
}
|
|
457
|
+
else if (dbError.detail?.includes('username')) {
|
|
458
|
+
this.logger?.warn?.(`Admin signup failed - username constraint violation: ${dto.username}`);
|
|
459
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.USERNAME_EXISTS, 'Username is already taken');
|
|
460
|
+
}
|
|
461
|
+
else if (dbError.detail?.includes('phone')) {
|
|
462
|
+
this.logger?.warn?.(`Admin signup failed - phone constraint violation: ${dto.phone}`);
|
|
463
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PHONE_EXISTS, 'Phone number is already registered');
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
this.logger?.error?.(`Admin signup failed - database constraint violation: ${dbError.message}`);
|
|
467
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.EMAIL_EXISTS, 'User with this information already exists', {
|
|
468
|
+
conflictType: 'unknown',
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Re-throw other database errors
|
|
473
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown database error';
|
|
474
|
+
this.logger?.error?.(`Admin signup failed - database error: ${errorMessage}`);
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
// No tokens, no challenge system, no verification emails - pure user creation
|
|
478
|
+
// Return sanitized user object (excludes passwordHash and other sensitive fields)
|
|
479
|
+
const userDto = user_response_dto_1.UserResponseDto.fromEntity(savedUser);
|
|
480
|
+
return {
|
|
481
|
+
user: userDto,
|
|
482
|
+
generatedPassword,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
// ============================================================================
|
|
292
486
|
// User Login
|
|
293
487
|
// ============================================================================
|
|
294
488
|
/**
|
|
@@ -794,10 +988,19 @@ class AuthService {
|
|
|
794
988
|
});
|
|
795
989
|
}
|
|
796
990
|
}
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
|
|
991
|
+
// ============================================================================
|
|
992
|
+
// Lifecycle Hook: afterLogin
|
|
993
|
+
// ============================================================================
|
|
994
|
+
if (this.config.hooks?.afterLogin) {
|
|
995
|
+
try {
|
|
996
|
+
await this.config.hooks.afterLogin(user, session);
|
|
997
|
+
}
|
|
998
|
+
catch (hookError) {
|
|
999
|
+
const errorMessage = hookError instanceof Error ? hookError.message : 'Unknown error';
|
|
1000
|
+
// Non-blocking: auth succeeded; hook errors should not break login
|
|
1001
|
+
this.logger?.error?.(`afterLogin hook failed (continuing): ${errorMessage}`, { error: hookError });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
801
1004
|
// ============================================================================
|
|
802
1005
|
// Trusted Device Token Management (Remember Device Feature)
|
|
803
1006
|
// ============================================================================
|
|
@@ -847,11 +1050,13 @@ class AuthService {
|
|
|
847
1050
|
isEmailVerified: userDto.isEmailVerified,
|
|
848
1051
|
isPhoneVerified: userDto.isPhoneVerified ?? undefined,
|
|
849
1052
|
socialProviders: userDto.socialProviders && userDto.socialProviders.length > 0 ? userDto.socialProviders : undefined,
|
|
1053
|
+
hasPasswordHash: userDto.hasPasswordHash,
|
|
850
1054
|
},
|
|
851
1055
|
accessToken: tokens.accessToken,
|
|
852
1056
|
refreshToken: tokens.refreshToken,
|
|
853
1057
|
accessTokenExpiresAt: accessTokenValidation.payload?.exp || 0,
|
|
854
1058
|
refreshTokenExpiresAt: refreshTokenValidation.payload?.exp || 0,
|
|
1059
|
+
authMethod: 'password',
|
|
855
1060
|
trusted: isTrusted, // Include trusted flag so frontend knows if device is already trusted
|
|
856
1061
|
// Include deviceToken - CookieTokenInterceptor will handle cookie/stripping based on @TokenDelivery decorator
|
|
857
1062
|
deviceToken,
|
|
@@ -3008,7 +3213,7 @@ class AuthService {
|
|
|
3008
3213
|
const attempts = await this.accountLockoutStorage.recordFailedAttempt(ipAddress);
|
|
3009
3214
|
// Lock IP if max attempts reached
|
|
3010
3215
|
if (attempts >= (this.config.lockout.maxAttempts || 5)) {
|
|
3011
|
-
await this.accountLockoutStorage.
|
|
3216
|
+
await this.accountLockoutStorage.lockIpAddress(ipAddress, this.config.lockout.duration || 900, // 15 minutes default
|
|
3012
3217
|
'Too many failed login attempts from this IP');
|
|
3013
3218
|
// // Execute hook with IP address
|
|
3014
3219
|
// if (this.config.hooks?.afterAccountLock) {
|
|
@@ -3016,10 +3221,19 @@ class AuthService {
|
|
|
3016
3221
|
// }
|
|
3017
3222
|
}
|
|
3018
3223
|
}
|
|
3019
|
-
//
|
|
3020
|
-
//
|
|
3021
|
-
//
|
|
3022
|
-
|
|
3224
|
+
// ============================================================================
|
|
3225
|
+
// Lifecycle Hook: afterLoginFailed
|
|
3226
|
+
// ============================================================================
|
|
3227
|
+
if (this.config.hooks?.afterLoginFailed) {
|
|
3228
|
+
try {
|
|
3229
|
+
await this.config.hooks.afterLoginFailed(identifier, reason || 'unknown');
|
|
3230
|
+
}
|
|
3231
|
+
catch (hookError) {
|
|
3232
|
+
const errorMessage = hookError instanceof Error ? hookError.message : 'Unknown error';
|
|
3233
|
+
// Non-blocking: login already failed; do not throw
|
|
3234
|
+
this.logger?.error?.(`afterLoginFailed hook failed (continuing): ${errorMessage}`, { error: hookError });
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3023
3237
|
}
|
|
3024
3238
|
/**
|
|
3025
3239
|
* Records a login attempt with client context.
|
|
@@ -3044,10 +3258,50 @@ class AuthService {
|
|
|
3044
3258
|
await this.loginAttemptRepository.save(attempt);
|
|
3045
3259
|
}
|
|
3046
3260
|
/**
|
|
3047
|
-
* Get user
|
|
3048
|
-
*
|
|
3049
|
-
*
|
|
3261
|
+
* Get user for authentication context
|
|
3262
|
+
*
|
|
3263
|
+
* Loads user by sub (external identifier) with all fields needed for auth context.
|
|
3264
|
+
* Computes hasPasswordHash from passwordHash, then removes passwordHash and other sensitive fields.
|
|
3265
|
+
*
|
|
3266
|
+
* This method is used by AuthHandler and AuthGuard to load authenticated users.
|
|
3267
|
+
* It ensures consistent user object shape across platforms (core + NestJS).
|
|
3268
|
+
*
|
|
3269
|
+
* @param sub - External user identifier (UUID)
|
|
3270
|
+
* @returns User object with hasPasswordHash flag, without sensitive fields
|
|
3271
|
+
* @throws {NAuthException} If user not found or account is inactive
|
|
3272
|
+
*
|
|
3273
|
+
* @example
|
|
3274
|
+
* ```typescript
|
|
3275
|
+
* const user = await authService.getUserForAuthContext('user-uuid-123');
|
|
3276
|
+
* // user.hasPasswordHash === true/false
|
|
3277
|
+
* // user.passwordHash === undefined (removed)
|
|
3278
|
+
* ```
|
|
3050
3279
|
*/
|
|
3280
|
+
async getUserForAuthContext(sub) {
|
|
3281
|
+
// Load user with all fields including passwordHash (needed to compute hasPasswordHash)
|
|
3282
|
+
const user = await this.userRepository.findOne({
|
|
3283
|
+
where: { sub },
|
|
3284
|
+
});
|
|
3285
|
+
if (!user) {
|
|
3286
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
3287
|
+
}
|
|
3288
|
+
if (!user.isActive) {
|
|
3289
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.ACCOUNT_INACTIVE, 'Account is not active');
|
|
3290
|
+
}
|
|
3291
|
+
// Compute hasPasswordHash from passwordHash
|
|
3292
|
+
const hasPasswordHash = Boolean(user.passwordHash);
|
|
3293
|
+
// Create safe user object without sensitive fields
|
|
3294
|
+
const safeUser = {
|
|
3295
|
+
...user,
|
|
3296
|
+
hasPasswordHash,
|
|
3297
|
+
};
|
|
3298
|
+
// Remove sensitive fields
|
|
3299
|
+
delete safeUser.passwordHash;
|
|
3300
|
+
delete safeUser.totpSecret;
|
|
3301
|
+
delete safeUser.backupCodes;
|
|
3302
|
+
delete safeUser.passwordHistory;
|
|
3303
|
+
return safeUser;
|
|
3304
|
+
}
|
|
3051
3305
|
async getUserById(dto) {
|
|
3052
3306
|
const user = (await this.userRepository.findOne({ where: { sub: dto.sub } }));
|
|
3053
3307
|
return user ? user_response_dto_1.UserResponseDto.fromEntity(user) : null;
|
|
@@ -3154,17 +3408,9 @@ class AuthService {
|
|
|
3154
3408
|
this.logger?.warn?.(`Password reset failed - user not found: ${dto.identifier}`);
|
|
3155
3409
|
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
3156
3410
|
}
|
|
3157
|
-
// ============================================================================
|
|
3158
|
-
// Validate User Can Have Password Reset
|
|
3159
|
-
// ============================================================================
|
|
3160
|
-
// CRITICAL PROTECTION: Only allow for users with password authentication
|
|
3161
|
-
// Pure social users cannot have password reset (they don't have passwords)
|
|
3162
|
-
if (!user.passwordHash) {
|
|
3163
|
-
this.logger?.warn?.(`Password reset failed - user doesn't have a password (pure social signup): ${user.sub}`);
|
|
3164
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_CHANGE_NOT_ALLOWED, 'Password reset not available. This account uses social authentication only and has no password.');
|
|
3165
|
-
}
|
|
3166
3411
|
const mustChangePassword = dto.mustChangePassword ?? true; // Default to true for security
|
|
3167
3412
|
const revokeSessions = dto.revokeSessions !== false;
|
|
3413
|
+
const wasSocialOnly = !user.passwordHash;
|
|
3168
3414
|
const { sessionsRevoked } = await this.updateUserPassword({
|
|
3169
3415
|
user,
|
|
3170
3416
|
newPassword: dto.newPassword,
|
|
@@ -3179,6 +3425,9 @@ class AuthService {
|
|
|
3179
3425
|
metadata: {
|
|
3180
3426
|
identifier: dto.identifier,
|
|
3181
3427
|
mustChangePassword,
|
|
3428
|
+
// WHY: Admins can set the first password for social-only accounts so users can login via either route later.
|
|
3429
|
+
// This flag helps downstream observability without exposing anything to clients.
|
|
3430
|
+
wasSocialOnly,
|
|
3182
3431
|
},
|
|
3183
3432
|
},
|
|
3184
3433
|
});
|
|
@@ -3227,34 +3476,11 @@ class AuthService {
|
|
|
3227
3476
|
if (!user) {
|
|
3228
3477
|
return response; // Non-enumerating
|
|
3229
3478
|
}
|
|
3230
|
-
//
|
|
3231
|
-
//
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
// ============================================================================
|
|
3236
|
-
// WHY: A malicious actor may spam forgot-password to learn about accounts or to harass users.
|
|
3237
|
-
// We keep the API response non-enumerating, but still record an audit event for observability.
|
|
3238
|
-
this.logger?.warn?.(`Password reset requested for social-only account; ignoring for user: ${user.sub}`);
|
|
3239
|
-
try {
|
|
3240
|
-
await this.auditService?.recordEvent({
|
|
3241
|
-
userId: user.id,
|
|
3242
|
-
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.PASSWORD_RESET_REQUESTED,
|
|
3243
|
-
eventStatus: 'SUSPICIOUS',
|
|
3244
|
-
authMethod: 'social',
|
|
3245
|
-
reason: 'forgot_password_social_only',
|
|
3246
|
-
description: 'Password reset requested for social-only account (ignored)',
|
|
3247
|
-
});
|
|
3248
|
-
}
|
|
3249
|
-
catch (auditError) {
|
|
3250
|
-
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
3251
|
-
this.logger?.error?.(`Failed to record PASSWORD_RESET_REQUESTED audit event: ${errorMessage}`, {
|
|
3252
|
-
error: auditError,
|
|
3253
|
-
userId: user.id,
|
|
3254
|
-
});
|
|
3255
|
-
}
|
|
3256
|
-
return response;
|
|
3257
|
-
}
|
|
3479
|
+
// ============================================================================
|
|
3480
|
+
// Allow social-only accounts to set their first password via forgot-password
|
|
3481
|
+
// ============================================================================
|
|
3482
|
+
// WHY: Social-first users commonly want to add a password later. The reset code proves possession
|
|
3483
|
+
// of the delivery channel (email/sms) and avoids weakening account security.
|
|
3258
3484
|
const verificationMethod = this.config.signup?.verificationMethod ?? 'email';
|
|
3259
3485
|
// ============================================================================
|
|
3260
3486
|
// Determine delivery channel
|
|
@@ -3320,7 +3546,7 @@ class AuthService {
|
|
|
3320
3546
|
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.SERVICE_UNAVAILABLE, 'Password reset is not available');
|
|
3321
3547
|
}
|
|
3322
3548
|
const user = await this.findUserByIdentifier(dto.identifier, this.config.login?.identifierType);
|
|
3323
|
-
if (!user
|
|
3549
|
+
if (!user) {
|
|
3324
3550
|
// Non-enumerating: treat as invalid code
|
|
3325
3551
|
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.PASSWORD_RESET_CODE_INVALID, 'Invalid password reset code');
|
|
3326
3552
|
}
|