@nauth-toolkit/core 0.1.0 → 0.1.3
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/LICENSE +90 -0
- package/README.md +30 -0
- package/package.json +7 -2
- package/jest.config.js +0 -15
- package/jest.setup.ts +0 -6
- package/src/adapters/database-columns.ts +0 -165
- package/src/adapters/express.adapter.ts +0 -385
- package/src/adapters/fastify.adapter.ts +0 -416
- package/src/adapters/index.ts +0 -16
- package/src/adapters/storage.factory.ts +0 -143
- package/src/bootstrap.ts +0 -374
- package/src/dto/auth-challenge.dto.ts +0 -231
- package/src/dto/auth-response.dto.ts +0 -253
- package/src/dto/challenge-response.dto.ts +0 -234
- package/src/dto/change-password-request.dto.ts +0 -50
- package/src/dto/change-password-response.dto.ts +0 -29
- package/src/dto/change-password.dto.ts +0 -57
- package/src/dto/error-response.dto.ts +0 -136
- package/src/dto/get-available-methods.dto.ts +0 -55
- package/src/dto/get-challenge-data-response.dto.ts +0 -28
- package/src/dto/get-challenge-data.dto.ts +0 -69
- package/src/dto/get-client-info.dto.ts +0 -104
- package/src/dto/get-device-token-response.dto.ts +0 -25
- package/src/dto/get-events-by-type.dto.ts +0 -76
- package/src/dto/get-ip-address-response.dto.ts +0 -24
- package/src/dto/get-mfa-status.dto.ts +0 -94
- package/src/dto/get-risk-assessment-history.dto.ts +0 -39
- package/src/dto/get-session-id-response.dto.ts +0 -25
- package/src/dto/get-setup-data-response.dto.ts +0 -31
- package/src/dto/get-setup-data.dto.ts +0 -75
- package/src/dto/get-suspicious-activity.dto.ts +0 -42
- package/src/dto/get-user-agent-response.dto.ts +0 -23
- package/src/dto/get-user-auth-history.dto.ts +0 -95
- package/src/dto/get-user-by-email.dto.ts +0 -61
- package/src/dto/get-user-by-id.dto.ts +0 -46
- package/src/dto/get-user-devices.dto.ts +0 -53
- package/src/dto/get-user-response.dto.ts +0 -17
- package/src/dto/has-provider.dto.ts +0 -56
- package/src/dto/index.ts +0 -57
- package/src/dto/is-trusted-device-response.dto.ts +0 -34
- package/src/dto/list-providers-response.dto.ts +0 -23
- package/src/dto/login.dto.ts +0 -95
- package/src/dto/logout-all-response.dto.ts +0 -24
- package/src/dto/logout-all.dto.ts +0 -65
- package/src/dto/logout-response.dto.ts +0 -25
- package/src/dto/logout.dto.ts +0 -64
- package/src/dto/refresh-token.dto.ts +0 -36
- package/src/dto/remove-devices.dto.ts +0 -85
- package/src/dto/resend-code-response.dto.ts +0 -32
- package/src/dto/resend-code.dto.ts +0 -51
- package/src/dto/reset-password.dto.ts +0 -115
- package/src/dto/respond-challenge.dto.ts +0 -272
- package/src/dto/set-mfa-exemption.dto.ts +0 -112
- package/src/dto/set-must-change-password-response.dto.ts +0 -27
- package/src/dto/set-must-change-password.dto.ts +0 -46
- package/src/dto/set-preferred-method.dto.ts +0 -80
- package/src/dto/setup-mfa.dto.ts +0 -98
- package/src/dto/signup.dto.ts +0 -174
- package/src/dto/social-auth.dto.ts +0 -422
- package/src/dto/trust-device-response.dto.ts +0 -30
- package/src/dto/trust-device.dto.ts +0 -9
- package/src/dto/update-user-attributes-request.dto.ts +0 -51
- package/src/dto/user-response.dto.ts +0 -138
- package/src/dto/user-update.dto.ts +0 -222
- package/src/dto/verify-email.dto.ts +0 -313
- package/src/dto/verify-mfa-code.dto.ts +0 -103
- package/src/dto/verify-phone-by-sub.dto.ts +0 -78
- package/src/dto/verify-phone.dto.ts +0 -245
- package/src/entities/auth-audit.entity.ts +0 -232
- package/src/entities/challenge-session.entity.ts +0 -116
- package/src/entities/index.ts +0 -29
- package/src/entities/login-attempt.entity.ts +0 -64
- package/src/entities/mfa-device.entity.ts +0 -151
- package/src/entities/rate-limit.entity.ts +0 -44
- package/src/entities/session.entity.ts +0 -180
- package/src/entities/social-account.entity.ts +0 -96
- package/src/entities/storage-lock.entity.ts +0 -39
- package/src/entities/trusted-device.entity.ts +0 -112
- package/src/entities/user.entity.ts +0 -243
- package/src/entities/verification-token.entity.ts +0 -141
- package/src/enums/auth-audit-event-type.enum.ts +0 -360
- package/src/enums/error-codes.enum.ts +0 -420
- package/src/enums/mfa-method.enum.ts +0 -97
- package/src/enums/risk-factor.enum.ts +0 -111
- package/src/exceptions/nauth.exception.ts +0 -231
- package/src/handlers/auth.handler.ts +0 -260
- package/src/handlers/client-info.handler.ts +0 -101
- package/src/handlers/csrf.handler.ts +0 -156
- package/src/handlers/token-delivery.handler.ts +0 -118
- package/src/index.ts +0 -118
- package/src/interfaces/client-info.interface.ts +0 -85
- package/src/interfaces/config.interface.ts +0 -2135
- package/src/interfaces/entities.interface.ts +0 -226
- package/src/interfaces/index.ts +0 -15
- package/src/interfaces/logger.interface.ts +0 -283
- package/src/interfaces/mfa-provider.interface.ts +0 -154
- package/src/interfaces/oauth.interface.ts +0 -148
- package/src/interfaces/provider.interface.ts +0 -47
- package/src/interfaces/social-auth-provider.interface.ts +0 -131
- package/src/interfaces/storage-adapter.interface.ts +0 -82
- package/src/interfaces/template.interface.ts +0 -510
- package/src/interfaces/token-verifier.interface.ts +0 -110
- package/src/internal.ts +0 -178
- package/src/platform/interfaces.ts +0 -299
- package/src/schemas/auth-config.schema.ts +0 -646
- package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
- package/src/services/adaptive-mfa-decision.service.ts +0 -457
- package/src/services/auth-audit.service.spec.ts +0 -675
- package/src/services/auth-audit.service.ts +0 -558
- package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
- package/src/services/auth-challenge-helper.service.ts +0 -825
- package/src/services/auth-flow-context-builder.service.ts +0 -520
- package/src/services/auth-flow-rules.ts +0 -202
- package/src/services/auth-flow-state-definitions.ts +0 -190
- package/src/services/auth-flow-state-machine.service.ts +0 -207
- package/src/services/auth-flow-state-machine.types.ts +0 -316
- package/src/services/auth.service.spec.ts +0 -4195
- package/src/services/auth.service.ts +0 -3727
- package/src/services/challenge.service.spec.ts +0 -1363
- package/src/services/challenge.service.ts +0 -696
- package/src/services/client-info.service.spec.ts +0 -572
- package/src/services/client-info.service.ts +0 -374
- package/src/services/csrf.service.ts +0 -54
- package/src/services/email-verification.service.spec.ts +0 -1229
- package/src/services/email-verification.service.ts +0 -578
- package/src/services/geo-location.service.spec.ts +0 -603
- package/src/services/geo-location.service.ts +0 -599
- package/src/services/index.ts +0 -13
- package/src/services/jwt.service.spec.ts +0 -882
- package/src/services/jwt.service.ts +0 -621
- package/src/services/mfa-base.service.spec.ts +0 -246
- package/src/services/mfa-base.service.ts +0 -611
- package/src/services/mfa.service.spec.ts +0 -693
- package/src/services/mfa.service.ts +0 -960
- package/src/services/password.service.spec.ts +0 -166
- package/src/services/password.service.ts +0 -309
- package/src/services/phone-verification.service.spec.ts +0 -1120
- package/src/services/phone-verification.service.ts +0 -751
- package/src/services/risk-detection.service.spec.ts +0 -1292
- package/src/services/risk-detection.service.ts +0 -1012
- package/src/services/risk-scoring.service.spec.ts +0 -204
- package/src/services/risk-scoring.service.ts +0 -131
- package/src/services/session.service.spec.ts +0 -1293
- package/src/services/session.service.ts +0 -803
- package/src/services/social-account.service.spec.ts +0 -725
- package/src/services/social-auth-base.service.spec.ts +0 -418
- package/src/services/social-auth-base.service.ts +0 -581
- package/src/services/social-auth.service.spec.ts +0 -238
- package/src/services/social-auth.service.ts +0 -436
- package/src/services/social-provider-registry.service.spec.ts +0 -238
- package/src/services/social-provider-registry.service.ts +0 -122
- package/src/services/trusted-device.service.spec.ts +0 -505
- package/src/services/trusted-device.service.ts +0 -339
- package/src/storage/account-lockout-storage.service.spec.ts +0 -310
- package/src/storage/account-lockout-storage.service.ts +0 -89
- package/src/storage/index.ts +0 -3
- package/src/storage/memory-storage.adapter.ts +0 -443
- package/src/storage/rate-limit-storage.service.spec.ts +0 -247
- package/src/storage/rate-limit-storage.service.ts +0 -38
- package/src/templates/html-template.engine.spec.ts +0 -161
- package/src/templates/html-template.engine.ts +0 -688
- package/src/templates/index.ts +0 -7
- package/src/utils/common-passwords.spec.ts +0 -230
- package/src/utils/common-passwords.ts +0 -170
- package/src/utils/context-storage.ts +0 -188
- package/src/utils/cookie-names.util.ts +0 -67
- package/src/utils/cookies.util.ts +0 -94
- package/src/utils/index.ts +0 -12
- package/src/utils/ip-extractor.spec.ts +0 -330
- package/src/utils/ip-extractor.ts +0 -220
- package/src/utils/nauth-logger.spec.ts +0 -388
- package/src/utils/nauth-logger.ts +0 -215
- package/src/utils/pii-redactor.spec.ts +0 -130
- package/src/utils/pii-redactor.ts +0 -288
- package/src/utils/setup/get-repositories.ts +0 -140
- package/src/utils/setup/init-services.ts +0 -422
- package/src/utils/setup/init-social.ts +0 -189
- package/src/utils/setup/init-storage.ts +0 -94
- package/src/utils/setup/register-mfa.ts +0 -165
- package/src/utils/setup/run-nauth-migrations.ts +0 -61
- package/src/utils/token-delivery-policy.ts +0 -38
- package/src/validators/template.validator.ts +0 -219
- package/tsconfig.json +0 -37
- package/tsconfig.lint.json +0 -6
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { PasswordService } from './password.service';
|
|
2
|
-
|
|
3
|
-
describe('PasswordService', () => {
|
|
4
|
-
let service: PasswordService;
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
service = new PasswordService();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
describe('hashPassword', () => {
|
|
11
|
-
it('should hash password using Argon2id', async () => {
|
|
12
|
-
const password = 'SecurePassword123!';
|
|
13
|
-
const hash = await service.hashPassword(password);
|
|
14
|
-
|
|
15
|
-
expect(hash).toBeDefined();
|
|
16
|
-
expect(hash).not.toBe(password);
|
|
17
|
-
expect(hash).toMatch(/^\$argon2id\$/);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should generate different hashes for same password', async () => {
|
|
21
|
-
const password = 'SecurePassword123!';
|
|
22
|
-
const hash1 = await service.hashPassword(password);
|
|
23
|
-
const hash2 = await service.hashPassword(password);
|
|
24
|
-
|
|
25
|
-
expect(hash1).not.toBe(hash2);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('verifyPassword', () => {
|
|
30
|
-
it('should verify correct password', async () => {
|
|
31
|
-
const password = 'SecurePassword123!';
|
|
32
|
-
const hash = await service.hashPassword(password);
|
|
33
|
-
const isValid = await service.verifyPassword(password, hash);
|
|
34
|
-
|
|
35
|
-
expect(isValid).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should reject incorrect password', async () => {
|
|
39
|
-
const password = 'SecurePassword123!';
|
|
40
|
-
const hash = await service.hashPassword(password);
|
|
41
|
-
const isValid = await service.verifyPassword('WrongPassword!', hash);
|
|
42
|
-
|
|
43
|
-
expect(isValid).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should return false for invalid hash', async () => {
|
|
47
|
-
const isValid = await service.verifyPassword('test', 'invalid-hash');
|
|
48
|
-
expect(isValid).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('validatePassword', () => {
|
|
53
|
-
it('should validate password meeting all requirements', async () => {
|
|
54
|
-
const result = await service.validatePassword('SecurePass123!');
|
|
55
|
-
|
|
56
|
-
expect(result.valid).toBe(true);
|
|
57
|
-
expect(result.errors.length).toBe(0);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should reject password too short', async () => {
|
|
61
|
-
const result = await service.validatePassword('Short1!');
|
|
62
|
-
|
|
63
|
-
expect(result.valid).toBe(false);
|
|
64
|
-
expect(result.errors).toContain('Password must be at least 8 characters long');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should reject password without uppercase', async () => {
|
|
68
|
-
const result = await service.validatePassword('password123!');
|
|
69
|
-
|
|
70
|
-
expect(result.valid).toBe(false);
|
|
71
|
-
expect(result.errors).toContain('Password must contain at least one uppercase letter');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should reject password without numbers', async () => {
|
|
75
|
-
const result = await service.validatePassword('PasswordTest!');
|
|
76
|
-
|
|
77
|
-
expect(result.valid).toBe(false);
|
|
78
|
-
expect(result.errors).toContain('Password must contain at least one number');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should reject password without special characters', async () => {
|
|
82
|
-
const result = await service.validatePassword('SecurePasswordABC123');
|
|
83
|
-
|
|
84
|
-
expect(result.valid).toBe(false);
|
|
85
|
-
expect(result.errors).toContain(
|
|
86
|
-
'Password must contain at least one special character (!@#$%^&*()_+=[{}|;:,.<>?-])',
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should reject common password', async () => {
|
|
91
|
-
const result = await service.validatePassword('password123');
|
|
92
|
-
|
|
93
|
-
expect(result.valid).toBe(false);
|
|
94
|
-
expect(result.errors).toContain('Password is too common and easy to guess');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should reject password containing username', async () => {
|
|
98
|
-
const result = await service.validatePassword('JohnDoe123!', {
|
|
99
|
-
username: 'johndoe',
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(result.valid).toBe(false);
|
|
103
|
-
expect(result.errors).toContain('Password must not contain your email or username');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should reject password containing email username', async () => {
|
|
107
|
-
const result = await service.validatePassword('TestUser123!', {
|
|
108
|
-
email: 'testuser@example.com',
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
expect(result.valid).toBe(false);
|
|
112
|
-
expect(result.errors).toContain('Password must not contain your email or username');
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('isPasswordInHistory', () => {
|
|
117
|
-
it('should detect password in history', async () => {
|
|
118
|
-
const password = 'OldPassword123!';
|
|
119
|
-
const hash = await service.hashPassword(password);
|
|
120
|
-
const history = [hash];
|
|
121
|
-
|
|
122
|
-
const isReused = await service.isPasswordInHistory(password, history);
|
|
123
|
-
|
|
124
|
-
expect(isReused).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should allow password not in history', async () => {
|
|
128
|
-
const oldPassword = 'OldPassword123!';
|
|
129
|
-
const newPassword = 'NewPassword123!';
|
|
130
|
-
const hash = await service.hashPassword(oldPassword);
|
|
131
|
-
const history = [hash];
|
|
132
|
-
|
|
133
|
-
const isReused = await service.isPasswordInHistory(newPassword, history);
|
|
134
|
-
|
|
135
|
-
expect(isReused).toBe(false);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should handle empty history', async () => {
|
|
139
|
-
const isReused = await service.isPasswordInHistory('Password123!', []);
|
|
140
|
-
expect(isReused).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('addToHistory', () => {
|
|
145
|
-
it('should add password to history', () => {
|
|
146
|
-
const history: string[] = [];
|
|
147
|
-
const newHash = 'new-hash';
|
|
148
|
-
|
|
149
|
-
const updated = service.addToHistory(history, newHash);
|
|
150
|
-
|
|
151
|
-
expect(updated).toContain(newHash);
|
|
152
|
-
expect(updated.length).toBe(1);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should maintain maximum history size', () => {
|
|
156
|
-
const history = ['hash1', 'hash2', 'hash3', 'hash4', 'hash5'];
|
|
157
|
-
const newHash = 'hash6';
|
|
158
|
-
|
|
159
|
-
const updated = service.addToHistory(history, newHash);
|
|
160
|
-
|
|
161
|
-
expect(updated.length).toBe(5);
|
|
162
|
-
expect(updated).not.toContain('hash1');
|
|
163
|
-
expect(updated).toContain('hash6');
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import * as argon2 from 'argon2';
|
|
2
|
-
import { PasswordConfig } from '../interfaces/config.interface';
|
|
3
|
-
import { loadCommonPasswords } from '../utils/common-passwords';
|
|
4
|
-
import { NAuthException } from '../exceptions/nauth.exception';
|
|
5
|
-
import { AuthErrorCode } from '../enums/error-codes.enum';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Validation result for password policy checks
|
|
9
|
-
*/
|
|
10
|
-
export interface PasswordValidationResult {
|
|
11
|
-
/** Whether the password passes all validation rules */
|
|
12
|
-
valid: boolean;
|
|
13
|
-
|
|
14
|
-
/** List of validation errors if any */
|
|
15
|
-
errors: string[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Default password hashing configuration
|
|
20
|
-
* Based on OWASP recommendations for 2025
|
|
21
|
-
*/
|
|
22
|
-
const DEFAULT_ARGON2_CONFIG = {
|
|
23
|
-
type: argon2.argon2id, // Hybrid mode (best security)
|
|
24
|
-
memoryCost: 65536, // 64 MB memory usage
|
|
25
|
-
timeCost: 3, // 3 iterations
|
|
26
|
-
parallelism: 2, // 2 parallel threads
|
|
27
|
-
hashLength: 32, // 256-bit hash output
|
|
28
|
-
} as const;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Password Service
|
|
32
|
-
*
|
|
33
|
-
* Handles all password-related operations including:
|
|
34
|
-
* - Hashing passwords with Argon2id
|
|
35
|
-
* - Verifying passwords against hashes
|
|
36
|
-
* - Validating password policy compliance
|
|
37
|
-
* - Checking password history to prevent reuse
|
|
38
|
-
*
|
|
39
|
-
* Security Features:
|
|
40
|
-
* - Argon2id hashing (winner of Password Hashing Competition)
|
|
41
|
-
* - Configurable password policy
|
|
42
|
-
* - Common password detection (10,000+ passwords loaded from file)
|
|
43
|
-
* - Password history tracking
|
|
44
|
-
* - Protection against timing attacks
|
|
45
|
-
*
|
|
46
|
-
* ⚠️ SECURITY FIX #8: Now loads 10K+ common passwords from bundled file
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```typescript
|
|
50
|
-
* const passwordService = new PasswordService(config);
|
|
51
|
-
*
|
|
52
|
-
* // Hash a password
|
|
53
|
-
* const hash = await passwordService.hashPassword('SecurePass123!');
|
|
54
|
-
*
|
|
55
|
-
* // Verify a password
|
|
56
|
-
* const isValid = await passwordService.verifyPassword('SecurePass123!', hash);
|
|
57
|
-
*
|
|
58
|
-
* // Validate password policy
|
|
59
|
-
* const validation = await passwordService.validatePassword('weak');
|
|
60
|
-
* if (!validation.valid) {
|
|
61
|
-
* logger.error('Password validation failed', { errors: validation.errors });
|
|
62
|
-
* }
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
export class PasswordService {
|
|
66
|
-
/** Password policy configuration */
|
|
67
|
-
private readonly config: Required<PasswordConfig>;
|
|
68
|
-
|
|
69
|
-
/** Common passwords Set (10K+ passwords loaded at startup) */
|
|
70
|
-
private readonly commonPasswords: Set<string>;
|
|
71
|
-
|
|
72
|
-
constructor(passwordConfig?: PasswordConfig) {
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// MEDIUM SECURITY FIX #8: Load Comprehensive Password List (10K+ passwords)
|
|
75
|
-
// ============================================================================
|
|
76
|
-
this.commonPasswords = loadCommonPasswords();
|
|
77
|
-
|
|
78
|
-
// Merge provided config with sensible defaults
|
|
79
|
-
this.config = {
|
|
80
|
-
minLength: passwordConfig?.minLength ?? 8,
|
|
81
|
-
maxLength: passwordConfig?.maxLength ?? 128,
|
|
82
|
-
requireUppercase: passwordConfig?.requireUppercase ?? true,
|
|
83
|
-
requireLowercase: passwordConfig?.requireLowercase ?? true,
|
|
84
|
-
requireNumbers: passwordConfig?.requireNumbers ?? true,
|
|
85
|
-
requireSpecialChars: passwordConfig?.requireSpecialChars ?? true,
|
|
86
|
-
specialChars: passwordConfig?.specialChars ?? '!@#$%^&*()_+=[{}|;:,.<>?-]', // Move - to end to avoid range interpretation
|
|
87
|
-
preventCommon: passwordConfig?.preventCommon ?? true,
|
|
88
|
-
preventUserInfo: passwordConfig?.preventUserInfo ?? true,
|
|
89
|
-
historyCount: passwordConfig?.historyCount ?? 5,
|
|
90
|
-
expiryDays: passwordConfig?.expiryDays ?? 0, // 0 = disabled
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ============================================================================
|
|
95
|
-
// Password Hashing
|
|
96
|
-
// ============================================================================
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Hash a password using Argon2id algorithm
|
|
100
|
-
*
|
|
101
|
-
* Argon2id is the recommended password hashing algorithm as of 2025.
|
|
102
|
-
* It combines Argon2i (resistant to side-channel attacks) and Argon2d
|
|
103
|
-
* (resistant to GPU cracking attacks).
|
|
104
|
-
*
|
|
105
|
-
* @param password - Plain text password to hash
|
|
106
|
-
* @returns Hashed password string (includes salt and algorithm parameters)
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* ```typescript
|
|
110
|
-
* const hash = await passwordService.hashPassword('MySecurePassword123!');
|
|
111
|
-
* // Returns: $argon2id$v=19$m=65536,t=3,p=4$...
|
|
112
|
-
* ```
|
|
113
|
-
*/
|
|
114
|
-
async hashPassword(password: string): Promise<string> {
|
|
115
|
-
try {
|
|
116
|
-
return await argon2.hash(password, DEFAULT_ARGON2_CONFIG);
|
|
117
|
-
} catch (error) {
|
|
118
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
119
|
-
throw new NAuthException(AuthErrorCode.INTERNAL_ERROR, `Failed to hash password: ${errorMessage}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Verify a password against its hash
|
|
125
|
-
*
|
|
126
|
-
* This method is resistant to timing attacks by using constant-time
|
|
127
|
-
* comparison internally via Argon2's verify function.
|
|
128
|
-
*
|
|
129
|
-
* @param password - Plain text password to verify
|
|
130
|
-
* @param hash - Hashed password to compare against
|
|
131
|
-
* @returns True if password matches hash, false otherwise
|
|
132
|
-
*
|
|
133
|
-
* @example
|
|
134
|
-
* ```typescript
|
|
135
|
-
* const isValid = await passwordService.verifyPassword(
|
|
136
|
-
* 'MyPassword123!',
|
|
137
|
-
* '$argon2id$v=19$m=65536,t=3,p=4$...'
|
|
138
|
-
* );
|
|
139
|
-
* ```
|
|
140
|
-
*/
|
|
141
|
-
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
142
|
-
try {
|
|
143
|
-
return await argon2.verify(hash, password);
|
|
144
|
-
} catch {
|
|
145
|
-
// If verification fails due to invalid hash format, return false
|
|
146
|
-
// rather than throwing (could be malformed data)
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ============================================================================
|
|
152
|
-
// Password Validation
|
|
153
|
-
// ============================================================================
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Validate a password against configured policy rules
|
|
157
|
-
*
|
|
158
|
-
* Checks multiple security criteria:
|
|
159
|
-
* - Length requirements (min/max)
|
|
160
|
-
* - Character complexity (uppercase, lowercase, numbers, special chars)
|
|
161
|
-
* - Common password detection
|
|
162
|
-
* - User information leakage (username/email in password)
|
|
163
|
-
*
|
|
164
|
-
* @param password - Password to validate
|
|
165
|
-
* @param userInfo - Optional user information to check against (email, username)
|
|
166
|
-
* @returns Validation result with any errors
|
|
167
|
-
*
|
|
168
|
-
* @example
|
|
169
|
-
* ```typescript
|
|
170
|
-
* const result = await passwordService.validatePassword('weak', {
|
|
171
|
-
* email: 'user@example.com',
|
|
172
|
-
* username: 'john'
|
|
173
|
-
* });
|
|
174
|
-
*
|
|
175
|
-
* if (!result.valid) {
|
|
176
|
-
* logger.error('Password validation failed', { errors: result.errors });
|
|
177
|
-
* // ['Password must be at least 8 characters', ...]
|
|
178
|
-
* }
|
|
179
|
-
* ```
|
|
180
|
-
*/
|
|
181
|
-
async validatePassword(
|
|
182
|
-
password: string,
|
|
183
|
-
userInfo?: { email?: string; username?: string },
|
|
184
|
-
): Promise<PasswordValidationResult> {
|
|
185
|
-
const errors: string[] = [];
|
|
186
|
-
|
|
187
|
-
// Check length requirements
|
|
188
|
-
if (password.length < this.config.minLength) {
|
|
189
|
-
errors.push(`Password must be at least ${this.config.minLength} characters long`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (password.length > this.config.maxLength) {
|
|
193
|
-
errors.push(`Password must not exceed ${this.config.maxLength} characters`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Check character complexity requirements
|
|
197
|
-
if (this.config.requireUppercase && !/[A-Z]/.test(password)) {
|
|
198
|
-
errors.push('Password must contain at least one uppercase letter');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (this.config.requireLowercase && !/[a-z]/.test(password)) {
|
|
202
|
-
errors.push('Password must contain at least one lowercase letter');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (this.config.requireNumbers && !/\d/.test(password)) {
|
|
206
|
-
errors.push('Password must contain at least one number');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (this.config.requireSpecialChars) {
|
|
210
|
-
// Use a more robust approach to check for special characters
|
|
211
|
-
const hasSpecialChar = this.config.specialChars.split('').some((char) => password.includes(char));
|
|
212
|
-
if (!hasSpecialChar) {
|
|
213
|
-
errors.push(`Password must contain at least one special character ${this.config.specialChars}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Check against common passwords (10K+ passwords loaded from file)
|
|
218
|
-
// TODO: this is not truly functional, need to work on it later
|
|
219
|
-
if (this.config.preventCommon) {
|
|
220
|
-
if (this.commonPasswords.has(password.toLowerCase())) {
|
|
221
|
-
errors.push('Password is too common and easy to guess');
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Check for user information in password
|
|
226
|
-
if (this.config.preventUserInfo && userInfo) {
|
|
227
|
-
const passwordLower = password.toLowerCase();
|
|
228
|
-
|
|
229
|
-
if (userInfo.email) {
|
|
230
|
-
const emailUsername = userInfo.email.split('@')[0].toLowerCase();
|
|
231
|
-
if (passwordLower.includes(emailUsername)) {
|
|
232
|
-
errors.push('Password must not contain your email or username');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (userInfo.username) {
|
|
237
|
-
const usernameLower = userInfo.username.toLowerCase();
|
|
238
|
-
if (passwordLower.includes(usernameLower)) {
|
|
239
|
-
errors.push('Password must not contain your email or username');
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
valid: errors.length === 0,
|
|
246
|
-
errors,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Check if a password has been used before (password history check)
|
|
252
|
-
*
|
|
253
|
-
* Prevents users from reusing recent passwords, which is a security
|
|
254
|
-
* best practice to limit the impact of compromised passwords.
|
|
255
|
-
*
|
|
256
|
-
* @param password - Plain text password to check
|
|
257
|
-
* @param passwordHistory - Array of previous password hashes
|
|
258
|
-
* @returns True if password was used before, false otherwise
|
|
259
|
-
*
|
|
260
|
-
* @example
|
|
261
|
-
* ```typescript
|
|
262
|
-
* const isReused = await passwordService.isPasswordInHistory(
|
|
263
|
-
* 'NewPassword123!',
|
|
264
|
-
* user.passwordHistory // Last 5 passwords
|
|
265
|
-
* );
|
|
266
|
-
*
|
|
267
|
-
* if (isReused) {
|
|
268
|
-
* throw new Error('Cannot reuse recent passwords');
|
|
269
|
-
* }
|
|
270
|
-
* ```
|
|
271
|
-
*/
|
|
272
|
-
async isPasswordInHistory(password: string, passwordHistory: string[]): Promise<boolean> {
|
|
273
|
-
// Check if password matches any of the historical passwords
|
|
274
|
-
for (const oldHash of passwordHistory) {
|
|
275
|
-
const matches = await this.verifyPassword(password, oldHash);
|
|
276
|
-
if (matches) {
|
|
277
|
-
return true;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Add a password hash to history, maintaining the configured limit
|
|
286
|
-
*
|
|
287
|
-
* @param currentHistory - Current password history array
|
|
288
|
-
* @param newHash - New password hash to add
|
|
289
|
-
* @returns Updated history array with new hash
|
|
290
|
-
*
|
|
291
|
-
* @example
|
|
292
|
-
* ```typescript
|
|
293
|
-
* user.passwordHistory = passwordService.addToHistory(
|
|
294
|
-
* user.passwordHistory,
|
|
295
|
-
* newPasswordHash
|
|
296
|
-
* );
|
|
297
|
-
* ```
|
|
298
|
-
*/
|
|
299
|
-
addToHistory(currentHistory: string[], newHash: string): string[] {
|
|
300
|
-
const history = [...currentHistory, newHash];
|
|
301
|
-
|
|
302
|
-
// Keep only the most recent N passwords (configured limit)
|
|
303
|
-
if (history.length > this.config.historyCount) {
|
|
304
|
-
return history.slice(-this.config.historyCount);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return history;
|
|
308
|
-
}
|
|
309
|
-
}
|