From 9446f3278c83820a8193038dff20c6a52f0e7910 Mon Sep 17 00:00:00 2001 From: VictorEPlus <137103422+VictorEPlus@users.noreply.github.com> Date: Thu, 9 Jul 2026 07:42:39 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A1=20fix:=20Normalize=20Email=20Case?= =?UTF-8?q?=20When=20Issuing=20Verification=20Tokens=20(#14172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user schema lowercases emails on save, but sendVerificationEmail stored the verification token with the raw registration email. For mixed-case registrations, verifyEmail's lookup by the lowercased DB email never matched the token, failing with 'No email verification data found'. Normalize once and use it for the link, recipient, and token. resendVerificationEmail already used the DB email and was unaffected. Co-authored-by: VictorEPlus --- api/server/services/AuthService.js | 11 ++++++----- api/server/services/AuthService.spec.js | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 8f6c281ee1..a4a59bb8b6 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -218,16 +218,17 @@ const createTokenHash = () => { */ const sendVerificationEmail = async (user) => { const [verifyToken, hash] = createTokenHash(); + const email = user.email.toLowerCase(); const verificationLink = `${ domains.client - }/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`; + }/verify?token=${verifyToken}&email=${encodeURIComponent(email)}`; await sendEmail({ - email: user.email, + email, subject: 'Verify your email', payload: { appName: process.env.APP_TITLE || 'LibreChat', - name: user.name || user.username || user.email, + name: user.name || user.username || email, verificationLink: verificationLink, year: new Date().getFullYear(), }, @@ -236,14 +237,14 @@ const sendVerificationEmail = async (user) => { await createToken({ userId: user._id, - email: user.email, + email, type: AuthTokenTypes.EMAIL_VERIFICATION, token: hash, createdAt: Date.now(), expiresIn: 900, }); - logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`); + logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${email}]`); }; /** diff --git a/api/server/services/AuthService.spec.js b/api/server/services/AuthService.spec.js index 03579e7278..ce4dd517ff 100644 --- a/api/server/services/AuthService.spec.js +++ b/api/server/services/AuthService.spec.js @@ -471,6 +471,31 @@ describe('registerUser', () => { }), ); }); + + it('normalizes mixed-case emails when issuing the verification token and link', async () => { + checkEmailConfig.mockReturnValue(true); + + const result = await registerUser({ + ...registrationPayload, + email: 'Mixed.Case@Example.com', + }); + + expect(result.status).toBe(200); + expect(createToken).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'mixed.case@example.com', + type: 'email_verification', + }), + ); + expect(sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'mixed.case@example.com', + payload: expect.objectContaining({ + verificationLink: expect.stringContaining(encodeURIComponent('mixed.case@example.com')), + }), + }), + ); + }); }); describe('verifyEmail public response handling', () => {