From 411b0ea414aa2839f01a5ef3a287c959171a02fd Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 24 Jun 2026 16:18:36 -0400 Subject: [PATCH 1/2] fix: allow delegated admin panel access --- src/server/auth.oauth.test.ts | 122 +++++++++++++++++++++++++++++++++- src/server/auth.ts | 14 ---- 2 files changed, 121 insertions(+), 15 deletions(-) diff --git a/src/server/auth.oauth.test.ts b/src/server/auth.oauth.test.ts index 21b9db2..435c5e8 100644 --- a/src/server/auth.oauth.test.ts +++ b/src/server/auth.oauth.test.ts @@ -44,7 +44,13 @@ vi.mock('./utils/refresh', () => ({ refreshAdminTokenDeduped: vi.fn(), })); -import { checkOpenIdFn, oauthExchangeFn } from './auth'; +import { + adminLoginFn, + adminVerify2FAFn, + checkOpenIdFn, + oauthExchangeFn, + verifyAdminTokenFn, +} from './auth'; function jsonResponse(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { @@ -53,6 +59,120 @@ function jsonResponse(status: number, body: unknown): Response { }); } +describe('adminLoginFn', () => { + beforeEach(() => { + fetchMock.mockReset(); + updateSession.mockReset(); + sessionState.data = {}; + vi.stubGlobal('fetch', fetchMock); + }); + + it('accepts a backend-approved delegated admin without requiring the ADMIN role', async () => { + const user = { id: 'user-1', role: 'department-admin', email: 'delegate@example.com' }; + fetchMock.mockResolvedValueOnce(jsonResponse(200, { token: 'jwt-token', user })); + + const result = await adminLoginFn({ + data: { email: 'delegate@example.com', password: 'password' }, + }); + + expect(result).toEqual({ error: false, user }); + expect(fetchMock).toHaveBeenCalledWith('http://librechat.test/api/admin/login/local', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'delegate@example.com', password: 'password' }), + }); + expect(updateSession).toHaveBeenCalledWith( + expect.objectContaining({ + user, + token: 'jwt-token', + tokenProvider: 'librechat', + }), + ); + }); +}); + +describe('adminVerify2FAFn', () => { + beforeEach(() => { + fetchMock.mockReset(); + updateSession.mockReset(); + sessionState.data = {}; + vi.stubGlobal('fetch', fetchMock); + }); + + it('accepts a backend-approved delegated admin after 2FA verification', async () => { + const user = { id: 'user-2', role: 'department-admin', email: 'delegate2@example.com' }; + fetchMock.mockResolvedValueOnce(jsonResponse(200, { token: 'jwt-token-2', user })); + + const result = await adminVerify2FAFn({ + data: { tempToken: 'temp-token', totpCode: '123456' }, + }); + + expect(result).toEqual({ error: false, user }); + expect(fetchMock).toHaveBeenCalledWith('http://librechat.test/api/auth/2fa/verify-temp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tempToken: 'temp-token', token: '123456' }), + }); + expect(updateSession).toHaveBeenCalledWith( + expect.objectContaining({ + user, + token: 'jwt-token-2', + tokenProvider: 'librechat', + }), + ); + }); +}); + +describe('verifyAdminTokenFn', () => { + beforeEach(() => { + fetchMock.mockReset(); + updateSession.mockReset(); + sessionState.data = {}; + vi.stubGlobal('fetch', fetchMock); + }); + + it('keeps a fresh delegated admin session without requiring the ADMIN role', async () => { + const user = { id: 'user-3', role: 'department-admin', email: 'delegate3@example.com' }; + sessionState.data = { + user, + token: 'jwt-token-3', + lastVerified: Date.now(), + lastActivity: Date.now(), + }; + + const result = await verifyAdminTokenFn(); + + expect(result).toEqual({ valid: true, user }); + expect(fetchMock).not.toHaveBeenCalled(); + expect(updateSession).toHaveBeenCalledWith({ lastActivity: expect.any(Number) }); + }); + + it('clears a delegated admin session when backend capability revalidation is denied', async () => { + const user = { id: 'user-4', role: 'department-admin', email: 'delegate4@example.com' }; + sessionState.data = { + user, + token: 'jwt-token-4', + lastVerified: 0, + lastActivity: Date.now(), + }; + fetchMock.mockResolvedValueOnce(jsonResponse(403, {})); + + const result = await verifyAdminTokenFn(); + + expect(result).toEqual({ valid: false, error: 'Admin privileges have been revoked' }); + expect(fetchMock).toHaveBeenCalledWith('http://librechat.test/api/admin/verify', { + headers: { Authorization: 'Bearer jwt-token-4' }, + }); + expect(updateSession).toHaveBeenCalledWith( + expect.objectContaining({ + token: undefined, + user: undefined, + refreshToken: undefined, + }), + ); + }); +}); + describe('oauthExchangeFn', () => { beforeEach(() => { fetchMock.mockReset(); diff --git a/src/server/auth.ts b/src/server/auth.ts index afd5b9f..04797f3 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -2,7 +2,6 @@ import { z } from 'zod'; import crypto from 'crypto'; import { redirect } from '@tanstack/react-router'; import { queryOptions } from '@tanstack/react-query'; -import { SystemRoles } from 'librechat-data-provider'; import { createServerFn } from '@tanstack/react-start'; import { getRequestHeader } from '@tanstack/react-start/server'; import type * as t from '@/types'; @@ -84,10 +83,6 @@ export const adminLoginFn = createServerFn({ method: 'POST' }) }; } - if (loginData.user.role !== SystemRoles.ADMIN) { - return { error: true, message: 'You do not have admin privileges' }; - } - const now = Date.now(); const session = await useAppSession(); await session.update({ @@ -140,10 +135,6 @@ export const adminVerify2FAFn = createServerFn({ method: 'POST' }) const verifyData = responseData as t.TwoFAVerifyResponse; - if (verifyData.user.role !== SystemRoles.ADMIN) { - return { error: true, message: 'You do not have admin privileges' }; - } - const now = Date.now(); const session = await useAppSession(); await session.update({ @@ -183,11 +174,6 @@ export const verifyAdminTokenFn = createServerFn({ method: 'GET' }).handler(asyn return { valid: false, error: 'No session found' }; } - if (user.role !== SystemRoles.ADMIN) { - await clearSession(session); - return { valid: false, error: 'Not an admin user' }; - } - const now = Date.now(); if (lastActivity && now - lastActivity > SESSION_CONFIG.idleTimeout) { From e79e10dd0885e69b99bdd4177d3a061971600084 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 24 Jun 2026 16:29:02 -0400 Subject: [PATCH 2/2] fix: revalidate admin 2fa sessions --- src/server/auth.oauth.test.ts | 29 ++++++++++++++++++++++++++--- src/server/auth.ts | 19 +++++++++++++++++-- src/types/server.ts | 4 ++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/server/auth.oauth.test.ts b/src/server/auth.oauth.test.ts index 435c5e8..a61f39b 100644 --- a/src/server/auth.oauth.test.ts +++ b/src/server/auth.oauth.test.ts @@ -101,26 +101,49 @@ describe('adminVerify2FAFn', () => { it('accepts a backend-approved delegated admin after 2FA verification', async () => { const user = { id: 'user-2', role: 'department-admin', email: 'delegate2@example.com' }; - fetchMock.mockResolvedValueOnce(jsonResponse(200, { token: 'jwt-token-2', user })); + const verifiedUser = { ...user, name: 'Delegated Admin' }; + fetchMock + .mockResolvedValueOnce(jsonResponse(200, { token: 'jwt-token-2', user })) + .mockResolvedValueOnce(jsonResponse(200, { user: verifiedUser })); const result = await adminVerify2FAFn({ data: { tempToken: 'temp-token', totpCode: '123456' }, }); - expect(result).toEqual({ error: false, user }); + expect(result).toEqual({ error: false, user: verifiedUser }); expect(fetchMock).toHaveBeenCalledWith('http://librechat.test/api/auth/2fa/verify-temp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tempToken: 'temp-token', token: '123456' }), }); + expect(fetchMock).toHaveBeenCalledWith('http://librechat.test/api/admin/verify', { + headers: { Authorization: 'Bearer jwt-token-2' }, + }); expect(updateSession).toHaveBeenCalledWith( expect.objectContaining({ - user, + user: verifiedUser, token: 'jwt-token-2', tokenProvider: 'librechat', }), ); }); + + it('rejects a 2FA token that does not pass admin capability revalidation', async () => { + const user = { id: 'user-2', role: 'regular-user', email: 'user@example.com' }; + fetchMock + .mockResolvedValueOnce(jsonResponse(200, { token: 'user-jwt-token', user })) + .mockResolvedValueOnce(jsonResponse(403, {})); + + const result = await adminVerify2FAFn({ + data: { tempToken: 'temp-token', totpCode: '123456' }, + }); + + expect(result).toEqual({ error: true, message: 'You do not have admin privileges' }); + expect(fetchMock).toHaveBeenCalledWith('http://librechat.test/api/admin/verify', { + headers: { Authorization: 'Bearer user-jwt-token' }, + }); + expect(updateSession).not.toHaveBeenCalled(); + }); }); describe('verifyAdminTokenFn', () => { diff --git a/src/server/auth.ts b/src/server/auth.ts index 04797f3..b2f405a 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -134,11 +134,26 @@ export const adminVerify2FAFn = createServerFn({ method: 'POST' }) } const verifyData = responseData as t.TwoFAVerifyResponse; + const adminVerifyResponse = await fetch(`${getServerApiUrl()}/api/admin/verify`, { + headers: { Authorization: `Bearer ${verifyData.token}` }, + }); + + if (!adminVerifyResponse.ok) { + if (adminVerifyResponse.status === 403) { + return { error: true, message: 'You do not have admin privileges' }; + } + if (adminVerifyResponse.status === 401) { + return { error: true, message: 'Session is no longer valid' }; + } + return { error: true, message: '2FA verification failed' }; + } + + const adminVerifyData = (await adminVerifyResponse.json()) as t.AdminVerifyResponse; const now = Date.now(); const session = await useAppSession(); await session.update({ - user: verifyData.user, + user: adminVerifyData.user, token: verifyData.token, refreshToken: extractCookieValue(response, 'refreshToken'), tokenProvider: 'librechat', @@ -146,7 +161,7 @@ export const adminVerify2FAFn = createServerFn({ method: 'POST' }) lastActivity: now, }); - return { error: false, user: verifyData.user }; + return { error: false, user: adminVerifyData.user }; } catch (error) { console.error('2FA verification error:', error); return { error: true, message: 'Verification failed. Please try again.' }; diff --git a/src/types/server.ts b/src/types/server.ts index 74ac34e..52cbd11 100644 --- a/src/types/server.ts +++ b/src/types/server.ts @@ -26,6 +26,10 @@ export interface TwoFAVerifyResponse { user: SerializableUser; } +export interface AdminVerifyResponse { + user: SerializableUser; +} + export interface OAuthExchangeResponse { token: string; refreshToken?: string;