const express = require('express'); const request = require('supertest'); const originalDomainClient = process.env.DOMAIN_CLIENT; process.env.DOMAIN_CLIENT = 'http://client.test'; const mockLogger = { warn: jest.fn(), error: jest.fn(), info: jest.fn(), debug: jest.fn(), }; const mockOAuthHandler = jest.fn((_req, res) => res.status(204).end()); const mockOpenIDCallbackMiddleware = jest.fn((_req, _res, next) => next()); let mockOpenIDCallbackAuthenticatorOptions; const mockCreateOpenIDCallbackAuthenticator = jest.fn((options) => { mockOpenIDCallbackAuthenticatorOptions = options; return mockOpenIDCallbackMiddleware; }); const mockBuildOAuthFailureLog = jest.fn(({ provider, req, err, info, defaultMessage }) => ({ provider, code: err?.code ?? info?.code ?? info?.error ?? req.query?.error, name: err?.name ?? info?.name, message: err?.message ?? info?.message ?? info?.error_description ?? req.query?.error_description ?? defaultMessage, cause_code: err?.cause?.code ?? info?.cause?.code, cause_name: err?.cause?.name ?? info?.cause?.name, has_code: req.query?.code != null, has_state: req.query?.state != null, query_error: req.query?.error, query_error_description: req.query?.error_description, path: req.path, forwarded_for: req.headers?.['x-forwarded-for'], user_agent: req.headers?.['user-agent'], })); const mockGetOAuthFailureMessage = jest.fn( (req) => req.session?.messages?.pop() ?? req.query?.error_description ?? req.query?.error ?? 'OAuth authentication failed', ); const mockRedirectToAuthFailure = jest.fn((res, { clientDomain, authFailedError }) => res.redirect(`${clientDomain}/login?redirect=false&error=${authFailedError}`), ); const mockPassportAuthenticate = jest.fn(() => (_req, _res, next) => next()); jest.mock('passport', () => ({ authenticate: (...args) => mockPassportAuthenticate(...args), })); jest.mock('openid-client', () => ({ randomState: jest.fn(() => 'random-state'), })); jest.mock('@librechat/data-schemas', () => ({ logger: mockLogger, })); jest.mock('librechat-data-provider', () => ({ ...jest.requireActual('librechat-data-provider'), ErrorTypes: { AUTH_FAILED: 'auth_failed', }, })); jest.mock('@librechat/api', () => ({ buildOAuthFailureLog: (...args) => mockBuildOAuthFailureLog(...args), createOpenIDCallbackAuthenticator: (...args) => mockCreateOpenIDCallbackAuthenticator(...args), createSetBalanceConfig: jest.fn(() => (_req, _res, next) => next()), getOAuthFailureMessage: (...args) => mockGetOAuthFailureMessage(...args), redirectToAuthFailure: (...args) => mockRedirectToAuthFailure(...args), })); jest.mock('~/server/middleware', () => ({ checkDomainAllowed: jest.fn((_req, _res, next) => next()), loginLimiter: jest.fn((_req, _res, next) => next()), logHeaders: jest.fn((_req, _res, next) => next()), })); jest.mock('~/server/controllers/auth/oauth', () => ({ createOAuthHandler: jest.fn(() => mockOAuthHandler), })); jest.mock('~/models', () => ({ findBalanceByUser: jest.fn(), upsertBalanceFields: jest.fn(), })); jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn(), })); afterAll(() => { if (originalDomainClient === undefined) { delete process.env.DOMAIN_CLIENT; return; } process.env.DOMAIN_CLIENT = originalDomainClient; }); function getOAuthRouter() { jest.resetModules(); return require('./oauth'); } function createApp(sessionMessages) { const app = express(); app.use((req, _res, next) => { if (sessionMessages) { req.session = { messages: [...sessionMessages] }; } next(); }); app.use('/oauth', getOAuthRouter()); app.use((err, _req, res, _next) => { res.status(500).json({ message: err.message }); }); return app; } describe('OAuth route failure logging', () => { beforeEach(() => { mockLogger.warn.mockClear(); mockLogger.error.mockClear(); mockLogger.info.mockClear(); mockLogger.debug.mockClear(); mockOAuthHandler.mockClear(); mockOpenIDCallbackMiddleware.mockClear(); mockBuildOAuthFailureLog.mockClear(); mockGetOAuthFailureMessage.mockClear(); mockRedirectToAuthFailure.mockClear(); mockPassportAuthenticate.mockClear(); mockOpenIDCallbackAuthenticatorOptions = undefined; mockPassportAuthenticate.mockImplementation(() => (_req, _res, next) => next()); mockOpenIDCallbackMiddleware.mockImplementation((_req, _res, next) => next()); }); it('wires the package OpenID callback middleware into the route', async () => { const app = createApp(); await request(app) .get('/oauth/openid/callback?code=secret-code&state=secret-state') .expect(204); expect(mockOpenIDCallbackAuthenticatorOptions).toEqual({ passport: expect.objectContaining({ authenticate: expect.any(Function) }), logger: mockLogger, clientDomain: 'http://client.test', authFailedError: 'auth_failed', }); expect(mockOpenIDCallbackMiddleware).toHaveBeenCalledWith( expect.any(Object), expect.any(Object), expect.any(Function), ); expect(mockOAuthHandler).toHaveBeenCalled(); }); it('logs structured fallback errors without using Unknown OAuth error', async () => { const app = createApp(); const response = await request(app) .get('/oauth/error?error=access_denied&error_description=Denied%20by%20provider') .set('x-forwarded-for', '203.0.113.10') .expect(302); expect(response.headers.location).toBe( 'http://client.test/login?redirect=false&error=auth_failed', ); expect(mockLogger.warn).toHaveBeenCalledWith( '[OAuth] Authentication failed', expect.objectContaining({ provider: 'unknown', code: 'access_denied', message: 'Denied by provider', query_error: 'access_denied', query_error_description: 'Denied by provider', has_code: false, has_state: false, forwarded_for: '203.0.113.10', }), ); expect(JSON.stringify(mockLogger.warn.mock.calls[0])).not.toContain('Unknown OAuth error'); }); });