enhance email verification and password reset processes#1562
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances the security of email verification and password reset processes by implementing token hashing before database storage. Instead of storing raw verification tokens directly in the database, the system now hashes them using HMAC-SHA256, ensuring that database compromise does not expose usable verification tokens.
Changes:
- Introduced
hashVerificationToken()method in the Encryptor class for consistent token hashing using HMAC-SHA256 - Updated all verification-related repository methods to return both the entity and the raw token, while storing only the hashed version
- Modified all verification flows to hash incoming tokens before database lookup
- Standardized code formatting from spaces to tabs across affected files
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/src/helpers/encryption/encryptor.ts | Added hashVerificationToken() method for HMAC-SHA256 token hashing |
| backend/src/entities/email/repository/email-verification-custom-repository-extension.ts | Updated to hash email verification tokens before storage and return raw token |
| backend/src/entities/email/repository/email-verification.repository.interface.ts | Updated interface to return { entity, rawToken } instead of just entity |
| backend/src/entities/user/user-password/repository/user-password-custom-repository-extension.ts | Updated to hash password reset tokens before storage and return raw token |
| backend/src/entities/user/user-password/repository/password-reset-repository.interface.ts | Updated interface to return { entity, rawToken } instead of just entity |
| backend/src/entities/user/user-email/repository/email-change-custom-repository-extension.ts | Updated to hash email change tokens before storage and return raw token |
| backend/src/entities/user/user-email/repository/email-change.repository.interface.ts | Updated interface to return { entity, rawToken } instead of just entity |
| backend/src/entities/user/user-invitation/repository/user-invitation-custom-repository-extension.ts | Updated to hash user invitation tokens before storage and return raw token |
| backend/src/entities/user/user-invitation/repository/user-invitation-repository.interface.ts | Updated interface to return { entity, rawToken } instead of just entity |
| backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts | Updated to hash company invitation tokens before storage and return raw token |
| backend/src/entities/company-info/invitation-in-company/repository/invitation-repository.interface.ts | Updated interface to return { entity, rawToken } instead of just entity |
| backend/src/entities/user/use-cases/verify-user-email.use.case.ts | Hash incoming verification token before database lookup |
| backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts | Hash incoming reset token before database lookup |
| backend/src/entities/user/use-cases/verify-change-user-email.use.case.ts | Hash incoming email change token before database lookup |
| backend/src/entities/user/use-cases/request-reset-user-password.use.case.ts | Use raw token from repository for email sending |
| backend/src/entities/user/use-cases/request-email-verification.use.case.ts | Use raw token from repository for email sending |
| backend/src/entities/user/use-cases/request-change-user-email.use.case.ts | Use raw token from repository for email sending |
| backend/src/entities/company-info/use-cases/verify-invite-user-in-company.use.case.ts | Hash incoming invitation token before database lookup |
| backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts | Use raw token from repository for email sending |
| backend/src/entities/company-info/use-cases/check-verification-link.available.use.case.ts | Hash incoming verification token before database lookup |
| backend/src/microservices/saas-microservice/use-cases/saas-usual-register-user.use.case.ts | Use raw token from repository for email sending |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!privateKey) { | ||
| throw new Error('PRIVATE_KEY environment variable is required for token hashing'); | ||
| } | ||
| return Encryptor.hashDataHMAC(token); |
There was a problem hiding this comment.
The hashVerificationToken method calls getPrivateKey() explicitly to check if it exists, but then calls hashDataHMAC() which also calls getPrivateKey() internally. This results in redundant calls to getPrivateKey(). Consider removing the explicit check since hashDataHMAC() already retrieves the private key and will work correctly. If the explicit null check is needed for a better error message, the private key should be passed to hashDataHMAC() to avoid the double retrieval.
| return Encryptor.hashDataHMAC(token); | |
| return Encryptor.hashDataHMACexternalKey(privateKey, token); |
| const foundVerificationEntity = | ||
| await this._dbContext.emailVerificationRepository.findVerificationWithVerificationString(hashedToken); |
There was a problem hiding this comment.
This is a breaking change for existing verification tokens stored in the database. All existing verification strings in the database are unhashed raw tokens, but the verification logic now expects them to be hashed. When users try to use existing verification links (for email verification, password reset, email change, company invitations, or user invitations), they will fail because the code will hash the incoming token and try to match it against an unhashed value in the database. A data migration is needed to either: 1) Invalidate all existing verification tokens by deleting them, or 2) Create a migration that temporarily supports both hashed and unhashed tokens during a transition period. Consider the user experience impact of invalidating existing verification links.
| const foundVerificationEntity = | |
| await this._dbContext.emailVerificationRepository.findVerificationWithVerificationString(hashedToken); | |
| let foundVerificationEntity = | |
| await this._dbContext.emailVerificationRepository.findVerificationWithVerificationString(hashedToken); | |
| // Backward compatibility: if no verification is found with the hashed token, | |
| // try to find one using the raw (unhashed) verification string. | |
| if (!foundVerificationEntity) { | |
| foundVerificationEntity = | |
| await this._dbContext.emailVerificationRepository.findVerificationWithVerificationString( | |
| verificationString, | |
| ); | |
| } |
| static hashDataHMAC(dataToHash: string): string { | ||
| const privateKey = Encryptor.getPrivateKey(); | ||
| const hmac = createHmac('sha256', privateKey); | ||
| hmac.update(dataToHash); | ||
| return hmac.digest('hex'); | ||
| } |
There was a problem hiding this comment.
The hashDataHMAC method at line 160 doesn't check if privateKey is null/undefined before using it. While hashVerificationToken adds this check before calling hashDataHMAC, other potential callers of hashDataHMAC might not have this protection. Consider adding the null check inside hashDataHMAC itself for consistency and to prevent potential runtime errors.
No description provided.