LibreChat/api/server/routes/admin/auth.refresh.test.js
Rayan Salhab 3551c1ba8e
🪧 fix: Guard Admin OAuth Routes When Providers Are Not Configured (#14507)
* fix: guard admin OpenID routes without config

* fix: guard remaining admin SSO routes without registered strategy

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-08-01 14:46:17 -04:00

440 lines
14 KiB
JavaScript

const express = require('express');
const request = require('supertest');
jest.mock('passport', () => ({
authenticate: jest.fn(() => (req, res, next) => next()),
_strategy: jest.fn(),
}));
jest.mock('openid-client', () => ({
refreshTokenGrant: jest.fn(),
}));
jest.mock('librechat-data-provider', () => ({
CacheKeys: { ADMIN_OAUTH_EXCHANGE: 'admin-oauth-exchange' },
}));
jest.mock('@librechat/data-schemas', () => ({
logger: {
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
},
DEFAULT_SESSION_EXPIRY: 60000,
SystemCapabilities: { ACCESS_ADMIN: 'ACCESS_ADMIN' },
getTenantId: jest.fn(() => undefined),
}));
jest.mock('@librechat/api', () => {
class AdminRefreshError extends Error {
constructor(code, status, message) {
super(message);
this.name = 'AdminRefreshError';
this.code = code;
this.status = status;
}
}
return {
isEnabled: jest.fn(),
getAdminPanelUrl: jest.fn(() => 'http://admin.example.com'),
exchangeAdminCode: jest.fn(),
createSetBalanceConfig: jest.fn(() => (req, res, next) => next()),
storeAndStripChallenge: jest.fn(),
tenantContextMiddleware: jest.fn((req, res, next) => next()),
preAuthTenantMiddleware: jest.fn((req, res, next) => next()),
applyAdminRefresh: jest.fn(),
AdminRefreshError,
buildOpenIDRefreshParams: jest.fn(() => {
const params = {};
if (process.env.OPENID_SCOPE) {
params.scope = process.env.OPENID_SCOPE;
}
if (process.env.OPENID_REFRESH_AUDIENCE) {
params.audience = process.env.OPENID_REFRESH_AUDIENCE;
}
return params;
}),
};
});
jest.mock('~/server/controllers/auth/LoginController', () => ({
loginController: jest.fn((req, res) => res.status(200).end()),
}));
jest.mock('~/server/middleware/roles/capabilities', () => ({
hasCapability: jest.fn(() => Promise.resolve(true)),
requireCapability: jest.fn(() => (req, res, next) => next()),
}));
jest.mock('~/server/controllers/auth/oauth', () => ({
createOAuthHandler: jest.fn(() => (req, res) => res.status(200).end()),
}));
jest.mock('~/models', () => ({
findBalanceByUser: jest.fn(),
findUsers: jest.fn(),
generateToken: jest.fn(() => Promise.resolve('minted-token')),
getUserById: jest.fn(),
upsertBalanceFields: jest.fn(),
}));
jest.mock('~/server/services/Config', () => ({
getAppConfig: jest.fn(),
}));
jest.mock('~/cache/getLogStores', () =>
jest.fn(() => ({
get: jest.fn(),
delete: jest.fn(),
})),
);
jest.mock('~/strategies', () => ({
getOpenIdConfig: jest.fn(),
}));
jest.mock('~/server/middleware', () => ({
logHeaders: jest.fn((req, res, next) => next()),
loginLimiter: jest.fn((req, res, next) => next()),
checkBan: jest.fn((req, res, next) => next()),
validateEmailLogin: jest.fn((req, res, next) => next()),
requireLocalAuth: jest.fn((req, res, next) => next()),
requireJwtAuth: jest.fn((req, res, next) => next()),
checkDomainAllowed: jest.fn((req, res, next) => next()),
}));
const passport = require('passport');
const openIdClient = require('openid-client');
const { logger } = require('@librechat/data-schemas');
const {
isEnabled,
applyAdminRefresh,
storeAndStripChallenge,
buildOpenIDRefreshParams,
} = require('@librechat/api');
const { getOpenIdConfig } = require('~/strategies');
const middleware = require('~/server/middleware');
const adminAuthRouter = require('./auth');
const ORIGINAL_OPENID_SCOPE = process.env.OPENID_SCOPE;
const ORIGINAL_OPENID_REFRESH_AUDIENCE = process.env.OPENID_REFRESH_AUDIENCE;
const ORIGINAL_SESSION_EXPIRY = process.env.SESSION_EXPIRY;
describe('admin auth OpenID route availability', () => {
let app;
beforeEach(() => {
jest.clearAllMocks();
app = express();
app.use(express.json());
app.use('/api/admin', adminAuthRouter);
});
it('returns not configured for the OpenID availability check when config lookup throws', async () => {
getOpenIdConfig.mockImplementation(() => {
throw new Error('OpenID client is not initialized. Please call setupOpenId first.');
});
const response = await request(app).get('/api/admin/oauth/openid/check');
expect(response.status).toBe(404);
expect(response.body).toEqual({
error: 'OpenID configuration not found',
error_code: 'OPENID_NOT_CONFIGURED',
});
});
it('does not start OpenID admin login when config lookup throws', async () => {
getOpenIdConfig.mockImplementation(() => {
throw new Error('OpenID client is not initialized. Please call setupOpenId first.');
});
const response = await request(app).get('/api/admin/oauth/openid');
expect(response.status).toBe(404);
expect(response.body).toEqual({
error: 'OpenID configuration not found',
error_code: 'OPENID_NOT_CONFIGURED',
});
expect(storeAndStripChallenge).not.toHaveBeenCalled();
expect(passport.authenticate).not.toHaveBeenCalled();
});
it('does not run OpenID admin callback auth when config lookup throws', async () => {
getOpenIdConfig.mockImplementation(() => {
throw new Error('OpenID client is not initialized. Please call setupOpenId first.');
});
const response = await request(app).get('/api/admin/oauth/openid/callback?state=state');
expect(response.status).toBe(404);
expect(response.body).toEqual({
error: 'OpenID configuration not found',
error_code: 'OPENID_NOT_CONFIGURED',
});
});
});
describe('admin auth social route availability', () => {
let app;
beforeEach(() => {
jest.clearAllMocks();
passport._strategy.mockReturnValue(undefined);
app = express();
app.use(express.json());
app.use('/api/admin', adminAuthRouter);
});
const startRoutes = [
['saml', 'SAML'],
['google', 'Google'],
['github', 'GitHub'],
['discord', 'Discord'],
['facebook', 'Facebook'],
['apple', 'Apple'],
];
it.each(startRoutes)(
'does not start %s admin login when the strategy is not registered',
async (path, provider) => {
const response = await request(app).get(`/api/admin/oauth/${path}`);
expect(response.status).toBe(404);
expect(response.body).toEqual({
error: `${provider} configuration not found`,
error_code: `${provider.toUpperCase()}_NOT_CONFIGURED`,
});
expect(storeAndStripChallenge).not.toHaveBeenCalled();
expect(passport.authenticate).not.toHaveBeenCalled();
},
);
const callbackRoutes = [
[
'saml',
'SAML',
(agent) => agent.post('/api/admin/oauth/saml/callback').send({ RelayState: 'state' }),
],
['google', 'Google', (agent) => agent.get('/api/admin/oauth/google/callback?state=state')],
['github', 'GitHub', (agent) => agent.get('/api/admin/oauth/github/callback?state=state')],
['discord', 'Discord', (agent) => agent.get('/api/admin/oauth/discord/callback?state=state')],
[
'facebook',
'Facebook',
(agent) => agent.get('/api/admin/oauth/facebook/callback?state=state'),
],
[
'apple',
'Apple',
(agent) => agent.post('/api/admin/oauth/apple/callback').send({ state: 'state' }),
],
];
it.each(callbackRoutes)(
'does not run %s admin callback auth when the strategy is not registered',
async (path, provider, makeRequest) => {
const response = await makeRequest(request(app));
expect(response.status).toBe(404);
expect(response.body).toEqual({
error: `${provider} configuration not found`,
error_code: `${provider.toUpperCase()}_NOT_CONFIGURED`,
});
},
);
it('starts admin login when the strategy is registered', async () => {
passport._strategy.mockReturnValue({ name: 'googleAdmin' });
storeAndStripChallenge.mockResolvedValue(true);
await request(app).get('/api/admin/oauth/google');
expect(storeAndStripChallenge).toHaveBeenCalledTimes(1);
expect(passport.authenticate).toHaveBeenCalledWith(
'googleAdmin',
expect.objectContaining({ session: false }),
);
});
});
describe('admin auth OpenID refresh route', () => {
const openIdConfig = {
serverMetadata: jest.fn(() => ({ issuer: 'https://issuer.example.com' })),
};
const tokenset = {
access_token: 'new-admin-access',
id_token: 'new-admin-id',
refresh_token: 'new-admin-refresh',
expires_in: 3600,
claims: jest.fn(() => ({ sub: 'admin-openid-id' })),
};
let app;
beforeEach(() => {
jest.clearAllMocks();
delete process.env.OPENID_SCOPE;
delete process.env.OPENID_REFRESH_AUDIENCE;
delete process.env.SESSION_EXPIRY;
app = express();
app.use(express.json());
app.use('/api/admin', adminAuthRouter);
isEnabled.mockReturnValue(true);
getOpenIdConfig.mockReturnValue(openIdConfig);
openIdClient.refreshTokenGrant.mockResolvedValue(tokenset);
applyAdminRefresh.mockResolvedValue({
token: 'admin-jwt',
refreshToken: 'new-admin-refresh',
user: { id: 'user-id', email: 'admin@example.com' },
expiresAt: 1234567890,
});
});
afterAll(() => {
if (ORIGINAL_OPENID_SCOPE === undefined) {
delete process.env.OPENID_SCOPE;
} else {
process.env.OPENID_SCOPE = ORIGINAL_OPENID_SCOPE;
}
if (ORIGINAL_OPENID_REFRESH_AUDIENCE === undefined) {
delete process.env.OPENID_REFRESH_AUDIENCE;
} else {
process.env.OPENID_REFRESH_AUDIENCE = ORIGINAL_OPENID_REFRESH_AUDIENCE;
}
if (ORIGINAL_SESSION_EXPIRY === undefined) {
delete process.env.SESSION_EXPIRY;
} else {
process.env.SESSION_EXPIRY = ORIGINAL_SESSION_EXPIRY;
}
});
it.each([
['scope-only', { OPENID_SCOPE: 'openid profile email' }, { scope: 'openid profile email' }],
[
'scope and audience',
{
OPENID_SCOPE: 'openid profile email',
OPENID_REFRESH_AUDIENCE: 'https://api.example.com',
},
{ scope: 'openid profile email', audience: 'https://api.example.com' },
],
[
'audience-only',
{ OPENID_REFRESH_AUDIENCE: 'https://api.example.com' },
{ audience: 'https://api.example.com' },
],
['empty audience', { OPENID_REFRESH_AUDIENCE: '' }, {}],
])('passes %s params to the OpenID refresh grant', async (_label, env, expectedParams) => {
Object.assign(process.env, env);
const response = await request(app)
.post('/api/admin/oauth/refresh')
.send({ refresh_token: 'incoming-refresh-token' });
expect(response.status).toBe(200);
expect(buildOpenIDRefreshParams).toHaveBeenCalledTimes(1);
expect(openIdClient.refreshTokenGrant).toHaveBeenCalledWith(
openIdConfig,
'incoming-refresh-token',
expectedParams,
);
expect(applyAdminRefresh).toHaveBeenCalledWith(
tokenset,
expect.any(Object),
expect.objectContaining({ previousRefreshToken: 'incoming-refresh-token' }),
);
});
it('returns the existing refresh failure response when the IdP rejects the grant', async () => {
openIdClient.refreshTokenGrant.mockRejectedValue({
code: 'invalid_grant',
name: 'OAuthError',
});
const response = await request(app)
.post('/api/admin/oauth/refresh')
.send({ refresh_token: 'incoming-refresh-token' });
expect(response.status).toBe(401);
expect(response.body).toEqual({
error: 'Refresh failed',
error_code: 'REFRESH_FAILED',
});
expect(applyAdminRefresh).not.toHaveBeenCalled();
});
it('keeps admin refresh diagnostics free of token and audience values', async () => {
process.env.OPENID_SCOPE = 'openid profile email';
process.env.OPENID_REFRESH_AUDIENCE = 'https://api.example.com';
await request(app)
.post('/api/admin/oauth/refresh')
.send({ refresh_token: 'incoming-refresh-token' });
expect(logger.debug).toHaveBeenCalledWith('[admin/oauth/refresh] OpenID refresh params', {
has_scope: true,
has_refresh_audience: true,
});
expect(logger.debug).toHaveBeenCalledWith('[admin/oauth/refresh] OpenID refresh succeeded', {
has_access_token: true,
has_id_token: true,
has_refresh_token: true,
expires_in: 3600,
});
const debugOutput = JSON.stringify(logger.debug.mock.calls);
expect(debugOutput).not.toContain('incoming-refresh-token');
expect(debugOutput).not.toContain('new-admin-access');
expect(debugOutput).not.toContain('new-admin-id');
expect(debugOutput).not.toContain('new-admin-refresh');
expect(debugOutput).not.toContain('https://api.example.com');
});
});
describe('admin local login route', () => {
let app;
beforeEach(() => {
jest.clearAllMocks();
app = express();
app.use(express.json());
app.use('/api/admin', adminAuthRouter);
});
it('applies the email login gate before local auth', async () => {
const response = await request(app).post('/api/admin/login/local').send({
email: 'admin@example.com',
password: 'password',
});
expect(response.status).toBe(200);
expect(middleware.validateEmailLogin).toHaveBeenCalledTimes(1);
expect(middleware.requireLocalAuth).toHaveBeenCalledTimes(1);
expect(middleware.validateEmailLogin.mock.invocationCallOrder[0]).toBeLessThan(
middleware.requireLocalAuth.mock.invocationCallOrder[0],
);
});
it('stops before local auth when the email login gate rejects the request', async () => {
middleware.validateEmailLogin.mockImplementationOnce((req, res) =>
res.status(403).json({ message: 'Email login is not allowed.' }),
);
const response = await request(app).post('/api/admin/login/local').send({
email: 'admin@example.com',
password: 'password',
});
expect(response.status).toBe(403);
expect(response.body).toEqual({ message: 'Email login is not allowed.' });
expect(middleware.requireLocalAuth).not.toHaveBeenCalled();
});
});