@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.
Files changed (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +30 -0
  3. package/package.json +7 -2
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. 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
- }