@nauth-toolkit/core 0.1.0 → 0.1.5

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 +9 -0
  3. package/package.json +8 -3
  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,578 +0,0 @@
1
- import { Repository, IsNull } from 'typeorm';
2
- import { IUser, IVerificationToken } from '../interfaces/entities.interface';
3
- import { BaseVerificationToken, BaseUser } from '../entities';
4
- import { EmailProvider } from '../interfaces/provider.interface';
5
- import { StorageAdapter } from '../interfaces/storage-adapter.interface';
6
- import { NAuthConfig } from '../interfaces/config.interface';
7
- import { ClientInfoService } from './client-info.service';
8
- import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service';
9
- import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
10
- import { NAuthException } from '../exceptions/nauth.exception';
11
- import { AuthErrorCode } from '../enums/error-codes.enum';
12
- import { NAuthLogger } from '../utils/nauth-logger';
13
- import {
14
- SendVerificationEmailDTO,
15
- SendVerificationEmailResponseDTO,
16
- VerifyEmailWithCodeDTO,
17
- VerifyEmailWithTokenDTO,
18
- ResendVerificationEmailDTO,
19
- ResendVerificationEmailResponseDTO,
20
- VerifyEmailResponseDTO,
21
- } from '../dto/verify-email.dto';
22
- import * as crypto from 'crypto';
23
-
24
- /**
25
- * Email Verification Service
26
- *
27
- * Handles email verification workflow:
28
- * - Generate verification codes
29
- * - Send verification emails
30
- * - Verify codes with token generation
31
- * - Resend with rate limiting
32
- *
33
- * Supports both code-based (6-digit OTP) and link-based verification.
34
- */
35
- export class EmailVerificationService {
36
- constructor(
37
- private readonly verificationTokenRepo: Repository<BaseVerificationToken>,
38
- private readonly userRepo: Repository<BaseUser>,
39
- private readonly emailProvider: EmailProvider,
40
- private readonly storageAdapter: StorageAdapter,
41
- private readonly config: NAuthConfig,
42
- private readonly clientInfoService: ClientInfoService,
43
- private readonly logger: NAuthLogger,
44
- private readonly auditService?: AuthAuditService, // Optional - audit trail service (enabled via config.auditLogs.enabled)
45
- ) {}
46
-
47
- /**
48
- * Send verification email to user
49
- * Generates a new verification code and sends it via email
50
- *
51
- * @param dto - Request DTO containing sub, baseUrl, and skipAlreadyVerifiedCheck
52
- * @returns Response DTO with verification token ID
53
- */
54
- async sendVerificationEmail(dto: SendVerificationEmailDTO): Promise<SendVerificationEmailResponseDTO> {
55
- const { sub, baseUrl, skipAlreadyVerifiedCheck = false, challengeSessionId } = dto;
56
- // Get rate limit configuration from config (moved to signup.emailVerification)
57
- const rateLimitMax = this.config.signup?.emailVerification?.rateLimitMax || 3;
58
- const rateLimitWindow = this.config.signup?.emailVerification?.rateLimitWindow || 3600; // 1 hour in seconds
59
-
60
- // Check rate limit - use sub for rate limiting
61
- const rateLimitKey = `email-verification:${sub}`;
62
-
63
- // Check if key exists and has valid TTL (not expired)
64
- const ttlBefore = await this.storageAdapter.ttl(rateLimitKey);
65
- // Window is expired if: key doesn't exist (-1), expired (<0), or TTL is longer than configured window (config changed)
66
- const isWindowExpired = ttlBefore === -1 || ttlBefore < 0 || ttlBefore > rateLimitWindow;
67
-
68
- // If TTL is longer than configured window (config changed), delete the old key to reset it
69
- // This ensures the new window uses the current rateLimitWindow instead of preserving old expiry
70
- if (ttlBefore > rateLimitWindow) {
71
- await this.storageAdapter.del(rateLimitKey);
72
- }
73
-
74
- // Increment counter (will reset to 1 if key expired or doesn't exist)
75
- // Pass TTL so new records are created with correct expiry immediately
76
- const currentCount = await this.storageAdapter.incr(rateLimitKey, isWindowExpired ? rateLimitWindow : undefined);
77
-
78
- // If we created a new window, log it
79
- if (isWindowExpired && currentCount === 1) {
80
- this.logger?.debug?.(
81
- `Rate limit window reset for email verification: sub=${sub}, window=${rateLimitWindow}s, max=${rateLimitMax}`,
82
- );
83
- }
84
-
85
- // Get actual TTL after setting expiry (for error message)
86
- const actualTtl = await this.storageAdapter.ttl(rateLimitKey);
87
-
88
- if (currentCount > rateLimitMax) {
89
- throw new NAuthException(
90
- AuthErrorCode.RATE_LIMIT_EMAIL,
91
- 'Too many verification emails sent. Please try again later.',
92
- {
93
- retryAfter: actualTtl > 0 ? actualTtl : rateLimitWindow,
94
- currentCount,
95
- maxAttempts: rateLimitMax,
96
- },
97
- );
98
- }
99
-
100
- // Check if user already has a pending verification token
101
- const user = (await this.userRepo.findOne({ where: { sub } })) as IUser | null;
102
- if (!user) {
103
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
104
- }
105
-
106
- // Check resend delay to prevent abuse
107
- const resendDelay = this.config.signup?.emailVerification?.resendDelay ?? 60; // 1 minute default
108
- const lastToken = (await this.verificationTokenRepo.findOne({
109
- where: { userId: user.id, type: 'email' },
110
- order: { createdAt: 'DESC' },
111
- })) as IVerificationToken | null;
112
-
113
- if (lastToken) {
114
- const secondsSinceLastSend = (Date.now() - lastToken.createdAt.getTime()) / 1000;
115
- if (secondsSinceLastSend < resendDelay) {
116
- const waitSeconds = Math.ceil(resendDelay - secondsSinceLastSend);
117
- throw new NAuthException(
118
- AuthErrorCode.RATE_LIMIT_RESEND,
119
- `Please wait ${waitSeconds} seconds before requesting another code`,
120
- {
121
- retryAfter: waitSeconds,
122
- resendDelay,
123
- },
124
- );
125
- }
126
- }
127
-
128
- // Only check "already verified" if not skipping (skip for MFA contexts where codes are needed even if email is verified)
129
- if (!skipAlreadyVerifiedCheck && user.isEmailVerified) {
130
- throw new NAuthException(AuthErrorCode.ALREADY_VERIFIED, 'Email already verified');
131
- }
132
-
133
- // Invalidate existing tokens - use internal id for database query
134
- await this.verificationTokenRepo.update(
135
- {
136
- userId: user.id, // Use internal id for foreign key query
137
- type: 'email',
138
- usedAt: IsNull(), // Only invalidate unused tokens
139
- },
140
- {
141
- usedAt: new Date(), // Mark as used to invalidate
142
- },
143
- );
144
-
145
- // Generate verification code (6 digits)
146
- const code = this.generateCode();
147
-
148
- // Generate verification token (for link-based verification)
149
- const token = this.generateToken();
150
- const tokenHash = this.hashToken(token);
151
-
152
- // Create verification token - use internal id for foreign key
153
- // Get client info internally
154
- const clientInfo = this.clientInfoService.get();
155
- const ipAddress = clientInfo.ipAddress;
156
- const userAgent = clientInfo.userAgent;
157
-
158
- const verificationToken = this.verificationTokenRepo.create({
159
- userId: user.id, // Use internal id for foreign key
160
- challengeSessionId: challengeSessionId ?? null, // Link to challenge session if provided
161
- type: 'email',
162
- token: tokenHash,
163
- code,
164
- expiresAt: new Date(Date.now() + (this.config.signup?.emailVerification?.expiresIn || 3600) * 1000), // Default: 1 hour
165
- attempts: 0,
166
- ipAddress,
167
- userAgent,
168
- });
169
-
170
- await this.verificationTokenRepo.save(verificationToken);
171
-
172
- // Generate verification link only if baseUrl is provided
173
- // Consumer apps can build their own verification links if needed
174
- const verificationLink = baseUrl ? `${baseUrl}/verify-email?token=${token}` : undefined;
175
-
176
- // Send email (link is optional - only sent if provided)
177
- await this.emailProvider.sendVerificationEmail(user.email, code, verificationLink);
178
-
179
- // ============================================================================
180
- // Audit: Record email verification request
181
- // ============================================================================
182
- try {
183
- await this.auditService?.recordEvent({
184
- userId: user.id,
185
- eventType: AuthAuditEventType.EMAIL_VERIFICATION_REQUESTED,
186
- eventStatus: 'INFO',
187
- metadata: {
188
- verificationTokenId: (verificationToken as unknown as IVerificationToken).id,
189
- },
190
- // Client info automatically included from context
191
- });
192
- } catch (auditError) {
193
- // Non-blocking: Log but continue
194
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
195
- this.logger?.error?.(`Failed to record EMAIL_VERIFICATION_REQUESTED audit event: ${errorMessage}`, {
196
- error: auditError,
197
- userId: user.id,
198
- });
199
- }
200
-
201
- return { tokenId: (verificationToken as unknown as IVerificationToken).id };
202
- }
203
-
204
- /**
205
- * Verify email with code (6-digit OTP)
206
- * Marks email as verified and activates user account
207
- *
208
- * @param dto - Request DTO containing email and code
209
- * @returns Response DTO with success message
210
- */
211
- async verifyEmailWithCode(dto: VerifyEmailWithCodeDTO): Promise<VerifyEmailResponseDTO> {
212
- const { email, code, challengeSessionId } = dto;
213
- // ============================================================================
214
- // Security: Rate limit configuration
215
- // IP-based rate limiting applies only to INVALID attempts to prevent brute force
216
- // Valid codes should not be blocked by IP rate limits
217
- // ============================================================================
218
- const maxAttemptsPerIP = this.config.signup?.emailVerification?.maxAttemptsPerIP ?? 20;
219
- const attemptWindow = this.config.signup?.emailVerification?.attemptWindow ?? 3600; // 1 hour
220
- const clientInfo = this.clientInfoService.get();
221
-
222
- // Find user by email
223
- const user = await this.userRepo.findOne({ where: { email } });
224
- if (!user) {
225
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
226
- }
227
-
228
- // Find active verification token
229
- // If challengeSessionId is provided, ensure token belongs to that specific session
230
- // This prevents old tokens from being used with new challenge sessions
231
- const whereClause = {
232
- userId: user.id,
233
- type: 'email' as const,
234
- code,
235
- usedAt: IsNull(),
236
- ...(challengeSessionId !== undefined && { challengeSessionId }), // Include if provided
237
- };
238
-
239
- const verificationToken = (await this.verificationTokenRepo.findOne({
240
- where: whereClause,
241
- })) as IVerificationToken | null;
242
-
243
- // ============================================================================
244
- // Security: Increment IP rate limit for invalid attempts only
245
- // This prevents brute force attacks while allowing valid codes through
246
- // ============================================================================
247
- const incrementIPRateLimit = async (): Promise<void> => {
248
- if (clientInfo.ipAddress) {
249
- const ipRateLimitKey = `verify-attempts:ip:${clientInfo.ipAddress}`;
250
- const ipAttempts = await this.storageAdapter.incr(ipRateLimitKey);
251
-
252
- if (ipAttempts === 1) {
253
- await this.storageAdapter.expire(ipRateLimitKey, attemptWindow);
254
- }
255
-
256
- if (ipAttempts > maxAttemptsPerIP) {
257
- throw new NAuthException(
258
- AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS,
259
- 'Too many verification attempts from this IP. Please try again later.',
260
- );
261
- }
262
- }
263
- };
264
-
265
- if (!verificationToken) {
266
- // Invalid attempt - increment IP rate limit
267
- await incrementIPRateLimit();
268
- throw new NAuthException(AuthErrorCode.VERIFICATION_CODE_INVALID, 'Invalid or expired verification code');
269
- }
270
-
271
- // Check expiry
272
- const isExpired = verificationToken.isExpired
273
- ? verificationToken.isExpired()
274
- : verificationToken.expiresAt < new Date();
275
- if (isExpired) {
276
- // Expired token - increment IP rate limit
277
- await incrementIPRateLimit();
278
- throw new NAuthException(AuthErrorCode.VERIFICATION_CODE_EXPIRED, 'Verification code has expired');
279
- }
280
-
281
- // Check max attempts (3 attempts)
282
- const maxAttemptsExceeded = verificationToken.maxAttemptsExceeded
283
- ? verificationToken.maxAttemptsExceeded(3)
284
- : verificationToken.attempts >= 3;
285
- if (maxAttemptsExceeded) {
286
- // Token exceeded max attempts - increment IP rate limit
287
- await incrementIPRateLimit();
288
- throw new NAuthException(
289
- AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS,
290
- 'Too many failed attempts. Request a new code.',
291
- );
292
- }
293
-
294
- // ============================================================================
295
- // Security: Rate limit verification attempts per user (for invalid attempts only)
296
- // This prevents users from exhausting their own tokens through repeated attempts
297
- // Valid codes should not be blocked by user rate limits
298
- // ============================================================================
299
- const maxAttemptsPerUser = this.config.signup?.emailVerification?.maxAttemptsPerUser ?? 10;
300
-
301
- // Helper to increment user rate limit (only for invalid attempts)
302
- const incrementUserRateLimit = async (): Promise<void> => {
303
- const userRateLimitKey = `verify-attempts:user:${user.id}`;
304
- const userAttempts = await this.storageAdapter.incr(userRateLimitKey);
305
-
306
- if (userAttempts === 1) {
307
- await this.storageAdapter.expire(userRateLimitKey, attemptWindow);
308
- }
309
-
310
- if (userAttempts > maxAttemptsPerUser) {
311
- throw new NAuthException(
312
- AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS,
313
- 'Too many verification attempts. Please try again later.',
314
- );
315
- }
316
- };
317
-
318
- // Increment attempts (even on success to prevent reuse)
319
- verificationToken.attempts += 1;
320
-
321
- // Invalid code - increment attempts and return false
322
- if (verificationToken.code !== code) {
323
- // Invalid code - increment both IP and user rate limits
324
- await incrementIPRateLimit();
325
- await incrementUserRateLimit();
326
-
327
- await this.verificationTokenRepo.save(verificationToken);
328
-
329
- // ============================================================================
330
- // Audit: Record email verification failure
331
- // ============================================================================
332
- try {
333
- await this.auditService?.recordEvent({
334
- userId: user.id as number,
335
- eventType: AuthAuditEventType.EMAIL_VERIFICATION_FAILED,
336
- eventStatus: 'FAILURE',
337
- reason: 'invalid_code',
338
- description: 'Invalid verification code provided',
339
- // Client info automatically included from context
340
- metadata: {
341
- verificationTokenId: (verificationToken as IVerificationToken).id,
342
- attempts: verificationToken.attempts,
343
- },
344
- });
345
- } catch (auditError) {
346
- // Non-blocking: Log but continue
347
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
348
- this.logger?.error?.(`Failed to record EMAIL_VERIFICATION_FAILED audit event: ${errorMessage}`, {
349
- error: auditError,
350
- userId: user.id,
351
- });
352
- }
353
-
354
- throw new NAuthException(AuthErrorCode.VERIFICATION_CODE_INVALID, 'Invalid verification code');
355
- }
356
-
357
- // ============================================================================
358
- // Code is valid - proceed without rate limit checks
359
- // Valid codes should always be allowed through
360
- // ============================================================================
361
-
362
- // Mark token as used
363
- verificationToken.usedAt = new Date();
364
- await this.verificationTokenRepo.save(verificationToken);
365
-
366
- // Update user - use internal id for database update
367
- await this.userRepo.update(user.id, {
368
- isEmailVerified: true,
369
- // Auto-activate if not already active
370
- isActive: true,
371
- });
372
-
373
- // ============================================================================
374
- // Audit: Record email verification success
375
- // ============================================================================
376
- try {
377
- await this.auditService?.recordEvent({
378
- userId: user.id,
379
- eventType: AuthAuditEventType.EMAIL_VERIFIED,
380
- eventStatus: 'SUCCESS',
381
- metadata: {
382
- verificationTokenId: (verificationToken as IVerificationToken).id,
383
- verificationMethod: 'code',
384
- // Client info automatically included from context
385
- },
386
- });
387
- } catch (auditError) {
388
- // Non-blocking: Log but continue
389
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
390
- this.logger?.error?.(`Failed to record EMAIL_VERIFIED audit event: ${errorMessage}`, {
391
- error: auditError,
392
- userId: user.id,
393
- });
394
- }
395
-
396
- // TODO: maybe refactor to return user save user query in parent function
397
- return {
398
- message: 'Email verified successfully. Please log in to continue.',
399
- };
400
- }
401
-
402
- /**
403
- * Verify email with link token
404
- * Marks email as verified and activates user account
405
- *
406
- * @param dto - Request DTO containing token
407
- * @returns Response DTO with success message
408
- */
409
- async verifyEmailWithToken(dto: VerifyEmailWithTokenDTO): Promise<VerifyEmailResponseDTO> {
410
- const { token } = dto;
411
- const tokenHash = this.hashToken(token);
412
-
413
- // Find verification token
414
- const verificationToken = (await this.verificationTokenRepo.findOne({
415
- where: {
416
- token: tokenHash,
417
- type: 'email',
418
- usedAt: IsNull(),
419
- },
420
- })) as IVerificationToken | null;
421
-
422
- if (!verificationToken) {
423
- throw new NAuthException(AuthErrorCode.VERIFICATION_CODE_INVALID, 'Invalid or expired verification link');
424
- }
425
-
426
- // Check expiry
427
- const isExpired = verificationToken.isExpired
428
- ? verificationToken.isExpired()
429
- : verificationToken.expiresAt < new Date();
430
- if (isExpired) {
431
- throw new NAuthException(AuthErrorCode.VERIFICATION_CODE_EXPIRED, 'Verification link has expired');
432
- }
433
-
434
- // Mark token as used
435
- verificationToken.usedAt = new Date();
436
- await this.verificationTokenRepo.save(verificationToken);
437
-
438
- // Get user for audit logging
439
- const user = (await this.userRepo.findOne({
440
- where: { id: verificationToken.userId },
441
- })) as IUser | null;
442
-
443
- // Update user
444
- await this.userRepo.update(verificationToken.userId, {
445
- isEmailVerified: true,
446
- // Auto-activate if not already active
447
- isActive: true,
448
- });
449
-
450
- // ============================================================================
451
- // Audit: Record email verification success (token-based)
452
- // ============================================================================
453
- if (user) {
454
- try {
455
- await this.auditService?.recordEvent({
456
- userId: user.id,
457
- eventType: AuthAuditEventType.EMAIL_VERIFIED,
458
- eventStatus: 'SUCCESS',
459
- metadata: {
460
- verificationTokenId: (verificationToken as IVerificationToken).id,
461
- verificationMethod: 'token',
462
- // Client info automatically included from context
463
- },
464
- });
465
- } catch (auditError) {
466
- // Non-blocking: Log but continue
467
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
468
- this.logger?.error?.(`Failed to record EMAIL_VERIFIED audit event (token-based): ${errorMessage}`, {
469
- error: auditError,
470
- userId: user?.id,
471
- });
472
- }
473
- }
474
-
475
- return {
476
- message: 'Email verified successfully. Please log in to continue.',
477
- };
478
- }
479
-
480
- /**
481
- * Resend verification email
482
- * Supports both sub and email-based resend
483
- *
484
- * @param dto - Request DTO containing sub or email, and optional baseUrl
485
- * @returns Response DTO with verification token ID
486
- */
487
- async resendVerificationEmail(dto: ResendVerificationEmailDTO): Promise<ResendVerificationEmailResponseDTO> {
488
- // Validate that either sub or email is provided
489
- if (!dto.sub && !dto.email) {
490
- throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'Either sub or email must be provided');
491
- }
492
-
493
- if (dto.sub) {
494
- return this.resendVerificationEmailBySub(dto.sub, dto.baseUrl);
495
- }
496
-
497
- return this.resendVerificationEmailByEmail(dto.email!, dto.baseUrl);
498
- }
499
-
500
- private async resendVerificationEmailBySub(
501
- sub: string,
502
- baseUrl?: string,
503
- ): Promise<ResendVerificationEmailResponseDTO> {
504
- // Get user by sub to get internal id
505
- const user = await this.userRepo.findOne({ where: { sub } });
506
- if (!user) {
507
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
508
- }
509
-
510
- // Check resend delay - use config value (default 60 seconds)
511
- const resendDelay = this.config.signup?.emailVerification?.resendDelay ?? 60;
512
- const lastToken = (await this.verificationTokenRepo.findOne({
513
- where: { userId: user.id, type: 'email' },
514
- order: { createdAt: 'DESC' },
515
- })) as IVerificationToken | null;
516
-
517
- if (lastToken) {
518
- const secondsSinceLastSend = (Date.now() - lastToken.createdAt.getTime()) / 1000;
519
- if (secondsSinceLastSend < resendDelay) {
520
- const waitSeconds = Math.ceil(resendDelay - secondsSinceLastSend);
521
- throw new NAuthException(
522
- AuthErrorCode.RATE_LIMIT_RESEND,
523
- `Please wait ${waitSeconds} seconds before requesting another code`,
524
- {
525
- retryAfter: waitSeconds,
526
- resendDelay,
527
- },
528
- );
529
- }
530
- }
531
-
532
- // Send new verification email - use sub (external identifier)
533
- // Preserve challengeSessionId from the last token to ensure verification succeeds
534
- const dto = Object.assign(new SendVerificationEmailDTO(), {
535
- sub,
536
- baseUrl,
537
- challengeSessionId: lastToken?.challengeSessionId ?? undefined,
538
- });
539
- return this.sendVerificationEmail(dto);
540
- }
541
-
542
- private async resendVerificationEmailByEmail(
543
- email: string,
544
- baseUrl?: string,
545
- ): Promise<ResendVerificationEmailResponseDTO> {
546
- const user = (await this.userRepo.findOne({ where: { email } })) as IUser | null;
547
- if (!user) {
548
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
549
- }
550
-
551
- return this.resendVerificationEmailBySub(user.sub, baseUrl);
552
- }
553
-
554
- /**
555
- * Generate 6-digit verification code
556
- * @returns 6-digit numeric code
557
- */
558
- private generateCode(): string {
559
- return Math.floor(100000 + Math.random() * 900000).toString();
560
- }
561
-
562
- /**
563
- * Generate secure random token
564
- * @returns Random token (32 bytes, hex encoded)
565
- */
566
- private generateToken(): string {
567
- return crypto.randomBytes(32).toString('hex');
568
- }
569
-
570
- /**
571
- * Hash token with SHA-256
572
- * @param token - Plain token
573
- * @returns Hashed token
574
- */
575
- private hashToken(token: string): string {
576
- return crypto.createHash('sha256').update(token).digest('hex');
577
- }
578
- }