From b01d34abe29152e6e7f763089c0d9a6e647d0dba Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 30 May 2026 10:04:50 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=AA=20fix:=20Preserve=20Trusted=20Regi?= =?UTF-8?q?stration=20Provider=20Overrides=20(#13307)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/services/AuthService.js | 13 +++-- api/server/services/AuthService.spec.js | 73 ++++++++++++++++++++++++- config/create-user.js | 5 +- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 24374ef4c6..6462c88682 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -191,13 +191,13 @@ const verifyEmail = async (req) => { /** * Register a new user. * @param {IUser} user - * @param {Partial} [additionalData={}] + * @param {Partial} [additionalData={}] Trusted server-provided fields, such as CLI overrides. * @returns {Promise<{status: number, message: string, user?: IUser}>} */ const registerUser = async (user, additionalData = {}) => { - const { error } = registerSchema.safeParse(user); - if (error) { - const errorMessage = errorsToString(error.errors); + const result = registerSchema.safeParse(user); + if (!result.success) { + const errorMessage = errorsToString(result.error.errors); logger.info( 'Route: register - Validation Error', { name: 'Request params:', value: user }, @@ -207,7 +207,8 @@ const registerUser = async (user, additionalData = {}) => { return { status: 404, message: errorMessage }; } - const { email, password, name, username, provider } = user; + const { email, password, name, username } = result.data; + const { provider, ...trustedAdditionalData } = additionalData ?? {}; let newUserId; try { @@ -245,7 +246,7 @@ const registerUser = async (user, additionalData = {}) => { avatar: null, role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER, password: bcrypt.hashSync(password, salt), - ...additionalData, + ...trustedAdditionalData, }; const emailEnabled = checkEmailConfig(); diff --git a/api/server/services/AuthService.spec.js b/api/server/services/AuthService.spec.js index 95a208c5c8..4ee7fc660f 100644 --- a/api/server/services/AuthService.spec.js +++ b/api/server/services/AuthService.spec.js @@ -42,11 +42,25 @@ jest.mock('~/models', () => ({ deleteUserById: jest.fn(), generateRefreshToken: jest.fn(), })); -jest.mock('~/strategies/validators', () => ({ registerSchema: { parse: jest.fn() } })); +jest.mock('~/strategies/validators', () => ({ + registerSchema: { + safeParse: jest.fn((user) => ({ + success: true, + data: { + name: user.name, + username: user.username, + email: user.email, + password: user.password, + confirm_password: user.confirm_password, + }, + })), + }, +})); jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn() })); jest.mock('~/server/utils', () => ({ sendEmail: jest.fn() })); const { + checkEmailConfig, shouldUseSecureCookie, isEmailDomainAllowed, resolveAppConfigForUser, @@ -58,6 +72,9 @@ const jwt = require('jsonwebtoken'); const { logger } = require('@librechat/data-schemas'); const { findUser, + createUser, + updateUser, + countUsers, getUserById, generateToken, generateRefreshToken, @@ -67,6 +84,7 @@ const { getAppConfig } = require('~/server/services/Config'); const { setOpenIDAuthTokens, requestPasswordReset, + registerUser, setAuthTokens, setCloudFrontAuthCookies, } = require('./AuthService'); @@ -381,6 +399,59 @@ describe('setOpenIDAuthTokens', () => { }); }); +describe('registerUser', () => { + const registrationPayload = { + name: 'Test User', + username: 'testuser', + email: 'test@example.com', + password: 'Password123!', + confirm_password: 'Password123!', + }; + + beforeEach(() => { + jest.clearAllMocks(); + process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN = 'false'; + checkEmailConfig.mockReturnValue(false); + isEmailDomainAllowed.mockReturnValue(true); + getAppConfig.mockResolvedValue({ + balance: { enabled: false }, + registration: { allowedDomains: [] }, + }); + findUser.mockResolvedValue(null); + countUsers.mockResolvedValue(1); + createUser.mockResolvedValue({ _id: 'new-user-id' }); + updateUser.mockResolvedValue({ _id: 'new-user-id' }); + }); + + it('ignores provider values from the public registration payload', async () => { + const result = await registerUser({ ...registrationPayload, provider: 'google' }); + + expect(result.status).toBe(200); + expect(createUser.mock.calls[0][0]).toEqual( + expect.objectContaining({ + email: registrationPayload.email, + provider: 'local', + }), + ); + }); + + it('allows trusted callers to set provider through additional data', async () => { + const result = await registerUser(registrationPayload, { + emailVerified: true, + provider: 'google', + }); + + expect(result.status).toBe(200); + expect(createUser.mock.calls[0][0]).toEqual( + expect.objectContaining({ + email: registrationPayload.email, + emailVerified: true, + provider: 'google', + }), + ); + }); +}); + describe('requestPasswordReset', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/config/create-user.js b/config/create-user.js index 3688d736e2..4d05211acc 100644 --- a/config/create-user.js +++ b/config/create-user.js @@ -103,10 +103,11 @@ or the user will need to attempt logging in to have a verification link sent to silentExit(1); } - const user = { email, password, name, username, confirm_password: password, provider }; + const user = { email, password, name, username, confirm_password: password }; + const additionalData = { emailVerified, ...(provider !== undefined ? { provider } : {}) }; let result; try { - result = await registerUser(user, { emailVerified }); + result = await registerUser(user, additionalData); } catch (error) { console.red('Error: ' + error.message); silentExit(1);