@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,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
|
-
}
|