🪧 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>
This commit is contained in:
Rayan Salhab 2026-08-01 21:46:17 +03:00 committed by GitHub
parent ad0f72dede
commit 3551c1ba8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 268 additions and 65 deletions

View file

@ -46,6 +46,39 @@ const setBalanceConfig = createSetBalanceConfig({
const router = express.Router();
function getOptionalOpenIdConfig() {
try {
return getOpenIdConfig();
} catch {
return null;
}
}
function requireOpenIdConfig(req, res, next) {
const openidConfig = getOptionalOpenIdConfig();
if (!openidConfig) {
return res.status(404).json({
error: 'OpenID configuration not found',
error_code: 'OPENID_NOT_CONFIGURED',
});
}
next();
}
/** Returns middleware that responds 404 when the given admin passport strategy is not registered. */
function requireAdminStrategy(strategyName, provider) {
return (req, res, next) => {
if (passport._strategy(strategyName)) {
return next();
}
return res.status(404).json({
error: `${provider} configuration not found`,
error_code: `${provider.toUpperCase()}_NOT_CONFIGURED`,
});
};
}
function resolveRequestOrigin(req) {
const originHeader = req.get('origin');
if (originHeader) {
@ -88,7 +121,7 @@ router.get('/verify', middleware.requireJwtAuth, requireAdminAccess, (req, res)
});
router.get('/oauth/openid/check', (req, res) => {
const openidConfig = getOpenIdConfig();
const openidConfig = getOptionalOpenIdConfig();
if (!openidConfig) {
return res.status(404).json({
error: 'OpenID configuration not found',
@ -145,7 +178,7 @@ function retrievePkceChallenge(provider) {
* OpenID Admin Routes
* */
router.get('/oauth/openid', async (req, res, next) => {
router.get('/oauth/openid', requireOpenIdConfig, async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'openid');
@ -167,6 +200,7 @@ router.get(
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
next();
},
requireOpenIdConfig,
passport.authenticate('openidAdmin', {
failureRedirect: `${getAdminPanelUrl()}/auth/openid/callback?error=auth_failed&error_description=Authentication+failed`,
failureMessage: true,
@ -184,7 +218,7 @@ router.get(
* SAML Admin Routes
* */
router.get('/oauth/saml', async (req, res, next) => {
router.get('/oauth/saml', requireAdminStrategy('samlAdmin', 'SAML'), async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'saml');
@ -206,6 +240,7 @@ router.post(
req.oauthState = typeof req.body.RelayState === 'string' ? req.body.RelayState : undefined;
next();
},
requireAdminStrategy('samlAdmin', 'SAML'),
passport.authenticate('samlAdmin', {
failureRedirect: `${getAdminPanelUrl()}/auth/saml/callback?error=auth_failed&error_description=Authentication+failed`,
failureMessage: true,
@ -223,22 +258,26 @@ router.post(
* Google Admin Routes
* */
router.get('/oauth/google', async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'google');
if (!stored) {
return res.redirect(
`${getAdminPanelUrl()}/auth/google/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
);
}
router.get(
'/oauth/google',
requireAdminStrategy('googleAdmin', 'Google'),
async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'google');
if (!stored) {
return res.redirect(
`${getAdminPanelUrl()}/auth/google/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
);
}
return passport.authenticate('googleAdmin', {
scope: ['openid', 'profile', 'email'],
session: false,
state,
})(req, res, next);
});
return passport.authenticate('googleAdmin', {
scope: ['openid', 'profile', 'email'],
session: false,
state,
})(req, res, next);
},
);
router.get(
'/oauth/google/callback',
@ -246,6 +285,7 @@ router.get(
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
next();
},
requireAdminStrategy('googleAdmin', 'Google'),
passport.authenticate('googleAdmin', {
failureRedirect: `${getAdminPanelUrl()}/auth/google/callback?error=auth_failed&error_description=Authentication+failed`,
failureMessage: true,
@ -263,22 +303,26 @@ router.get(
* GitHub Admin Routes
* */
router.get('/oauth/github', async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'github');
if (!stored) {
return res.redirect(
`${getAdminPanelUrl()}/auth/github/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
);
}
router.get(
'/oauth/github',
requireAdminStrategy('githubAdmin', 'GitHub'),
async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'github');
if (!stored) {
return res.redirect(
`${getAdminPanelUrl()}/auth/github/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
);
}
return passport.authenticate('githubAdmin', {
scope: ['user:email', 'read:user'],
session: false,
state,
})(req, res, next);
});
return passport.authenticate('githubAdmin', {
scope: ['user:email', 'read:user'],
session: false,
state,
})(req, res, next);
},
);
router.get(
'/oauth/github/callback',
@ -286,6 +330,7 @@ router.get(
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
next();
},
requireAdminStrategy('githubAdmin', 'GitHub'),
passport.authenticate('githubAdmin', {
failureRedirect: `${getAdminPanelUrl()}/auth/github/callback?error=auth_failed&error_description=Authentication+failed`,
failureMessage: true,
@ -303,22 +348,26 @@ router.get(
* Discord Admin Routes
* */
router.get('/oauth/discord', async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'discord');
if (!stored) {
return res.redirect(
`${getAdminPanelUrl()}/auth/discord/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
);
}
router.get(
'/oauth/discord',
requireAdminStrategy('discordAdmin', 'Discord'),
async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'discord');
if (!stored) {
return res.redirect(
`${getAdminPanelUrl()}/auth/discord/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
);
}
return passport.authenticate('discordAdmin', {
scope: ['identify', 'email'],
session: false,
state,
})(req, res, next);
});
return passport.authenticate('discordAdmin', {
scope: ['identify', 'email'],
session: false,
state,
})(req, res, next);
},
);
router.get(
'/oauth/discord/callback',
@ -326,6 +375,7 @@ router.get(
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
next();
},
requireAdminStrategy('discordAdmin', 'Discord'),
passport.authenticate('discordAdmin', {
failureRedirect: `${getAdminPanelUrl()}/auth/discord/callback?error=auth_failed&error_description=Authentication+failed`,
failureMessage: true,
@ -343,22 +393,26 @@ router.get(
* Facebook Admin Routes
* */
router.get('/oauth/facebook', async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'facebook');
if (!stored) {
return res.redirect(
`${getAdminPanelUrl()}/auth/facebook/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
);
}
router.get(
'/oauth/facebook',
requireAdminStrategy('facebookAdmin', 'Facebook'),
async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'facebook');
if (!stored) {
return res.redirect(
`${getAdminPanelUrl()}/auth/facebook/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
);
}
return passport.authenticate('facebookAdmin', {
scope: ['public_profile'],
session: false,
state,
})(req, res, next);
});
return passport.authenticate('facebookAdmin', {
scope: ['public_profile'],
session: false,
state,
})(req, res, next);
},
);
router.get(
'/oauth/facebook/callback',
@ -366,6 +420,7 @@ router.get(
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
next();
},
requireAdminStrategy('facebookAdmin', 'Facebook'),
passport.authenticate('facebookAdmin', {
failureRedirect: `${getAdminPanelUrl()}/auth/facebook/callback?error=auth_failed&error_description=Authentication+failed`,
failureMessage: true,
@ -383,7 +438,7 @@ router.get(
* Apple Admin Routes (POST callback)
* */
router.get('/oauth/apple', async (req, res, next) => {
router.get('/oauth/apple', requireAdminStrategy('appleAdmin', 'Apple'), async (req, res, next) => {
const state = generateState();
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
const stored = await storeAndStripChallenge(cache, req, state, 'apple');
@ -405,6 +460,7 @@ router.post(
req.oauthState = typeof req.body.state === 'string' ? req.body.state : undefined;
next();
},
requireAdminStrategy('appleAdmin', 'Apple'),
passport.authenticate('appleAdmin', {
failureRedirect: `${getAdminPanelUrl()}/auth/apple/callback?error=auth_failed&error_description=Authentication+failed`,
failureMessage: true,

View file

@ -3,6 +3,7 @@ const request = require('supertest');
jest.mock('passport', () => ({
authenticate: jest.fn(() => (req, res, next) => next()),
_strategy: jest.fn(),
}));
jest.mock('openid-client', () => ({
@ -103,9 +104,15 @@ jest.mock('~/server/middleware', () => ({
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, buildOpenIDRefreshParams } = require('@librechat/api');
const {
isEnabled,
applyAdminRefresh,
storeAndStripChallenge,
buildOpenIDRefreshParams,
} = require('@librechat/api');
const { getOpenIdConfig } = require('~/strategies');
const middleware = require('~/server/middleware');
const adminAuthRouter = require('./auth');
@ -114,6 +121,146 @@ 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' })),