mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-05-13 16:07:30 +00:00
622 lines
21 KiB
JavaScript
622 lines
21 KiB
JavaScript
const express = require('express');
|
|
const passport = require('passport');
|
|
const crypto = require('node:crypto');
|
|
const openIdClient = require('openid-client');
|
|
const { CacheKeys } = require('librechat-data-provider');
|
|
const {
|
|
logger,
|
|
DEFAULT_SESSION_EXPIRY,
|
|
SystemCapabilities,
|
|
getTenantId,
|
|
} = require('@librechat/data-schemas');
|
|
const {
|
|
isEnabled,
|
|
getAdminPanelUrl,
|
|
exchangeAdminCode,
|
|
createSetBalanceConfig,
|
|
storeAndStripChallenge,
|
|
tenantContextMiddleware,
|
|
preAuthTenantMiddleware,
|
|
applyAdminRefresh,
|
|
AdminRefreshError,
|
|
buildOpenIDRefreshParams,
|
|
} = require('@librechat/api');
|
|
const { loginController } = require('~/server/controllers/auth/LoginController');
|
|
const { hasCapability, requireCapability } = require('~/server/middleware/roles/capabilities');
|
|
const { createOAuthHandler } = require('~/server/controllers/auth/oauth');
|
|
const {
|
|
findBalanceByUser,
|
|
findUsers,
|
|
generateToken,
|
|
getUserById,
|
|
upsertBalanceFields,
|
|
} = require('~/models');
|
|
const { getAppConfig } = require('~/server/services/Config');
|
|
const getLogStores = require('~/cache/getLogStores');
|
|
const { getOpenIdConfig } = require('~/strategies');
|
|
const middleware = require('~/server/middleware');
|
|
|
|
const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
|
|
|
|
const setBalanceConfig = createSetBalanceConfig({
|
|
getAppConfig,
|
|
findBalanceByUser,
|
|
upsertBalanceFields,
|
|
});
|
|
|
|
const router = express.Router();
|
|
|
|
function resolveRequestOrigin(req) {
|
|
const originHeader = req.get('origin');
|
|
if (originHeader) {
|
|
try {
|
|
return new URL(originHeader).origin;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
const refererHeader = req.get('referer');
|
|
if (!refererHeader) {
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
return new URL(refererHeader).origin;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
router.post(
|
|
'/login/local',
|
|
middleware.logHeaders,
|
|
middleware.loginLimiter,
|
|
middleware.checkBan,
|
|
middleware.requireLocalAuth,
|
|
tenantContextMiddleware,
|
|
requireAdminAccess,
|
|
setBalanceConfig,
|
|
loginController,
|
|
);
|
|
|
|
router.get('/verify', middleware.requireJwtAuth, requireAdminAccess, (req, res) => {
|
|
const { password: _p, totpSecret: _t, __v, ...user } = req.user;
|
|
user.id = user._id.toString();
|
|
res.status(200).json({ user });
|
|
});
|
|
|
|
router.get('/oauth/openid/check', (req, res) => {
|
|
const openidConfig = getOpenIdConfig();
|
|
if (!openidConfig) {
|
|
return res.status(404).json({
|
|
error: 'OpenID configuration not found',
|
|
error_code: 'OPENID_NOT_CONFIGURED',
|
|
});
|
|
}
|
|
res.status(200).json({ message: 'OpenID check successful' });
|
|
});
|
|
|
|
/**
|
|
* Generates a random hex state string for OAuth flows.
|
|
* @returns {string} A 32-byte random hex string.
|
|
*/
|
|
function generateState() {
|
|
return crypto.randomBytes(32).toString('hex');
|
|
}
|
|
|
|
/**
|
|
* Middleware to retrieve PKCE challenge from cache using the OAuth state.
|
|
* Reads state from req.oauthState (set by a preceding middleware).
|
|
* @param {string} provider - Provider name for logging.
|
|
* @returns {Function} Express middleware.
|
|
*/
|
|
function retrievePkceChallenge(provider) {
|
|
return async (req, res, next) => {
|
|
if (!req.oauthState) {
|
|
return next();
|
|
}
|
|
try {
|
|
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
|
|
const challenge = await cache.get(`pkce:${req.oauthState}`);
|
|
if (challenge) {
|
|
req.pkceChallenge = challenge;
|
|
await cache.delete(`pkce:${req.oauthState}`);
|
|
} else {
|
|
logger.warn(
|
|
`[admin/oauth/${provider}/callback] State present but no PKCE challenge found; PKCE will not be enforced for this request`,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
logger.error(
|
|
`[admin/oauth/${provider}/callback] Failed to retrieve PKCE challenge, aborting:`,
|
|
err,
|
|
);
|
|
return res.redirect(
|
|
`${getAdminPanelUrl()}/auth/${provider}/callback?error=pkce_retrieval_failed&error_description=Failed+to+retrieve+PKCE+challenge`,
|
|
);
|
|
}
|
|
next();
|
|
};
|
|
}
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* OpenID Admin Routes
|
|
* ────────────────────────────────────────────── */
|
|
|
|
router.get('/oauth/openid', async (req, res, next) => {
|
|
const state = generateState();
|
|
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
|
|
const stored = await storeAndStripChallenge(cache, req, state, 'openid');
|
|
if (!stored) {
|
|
return res.redirect(
|
|
`${getAdminPanelUrl()}/auth/openid/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
|
|
);
|
|
}
|
|
|
|
return passport.authenticate('openidAdmin', {
|
|
session: false,
|
|
state,
|
|
})(req, res, next);
|
|
});
|
|
|
|
router.get(
|
|
'/oauth/openid/callback',
|
|
(req, res, next) => {
|
|
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
|
|
next();
|
|
},
|
|
passport.authenticate('openidAdmin', {
|
|
failureRedirect: `${getAdminPanelUrl()}/auth/openid/callback?error=auth_failed&error_description=Authentication+failed`,
|
|
failureMessage: true,
|
|
session: false,
|
|
}),
|
|
tenantContextMiddleware,
|
|
retrievePkceChallenge('openid'),
|
|
requireAdminAccess,
|
|
setBalanceConfig,
|
|
middleware.checkDomainAllowed,
|
|
createOAuthHandler(`${getAdminPanelUrl()}/auth/openid/callback`),
|
|
);
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* SAML Admin Routes
|
|
* ────────────────────────────────────────────── */
|
|
|
|
router.get('/oauth/saml', async (req, res, next) => {
|
|
const state = generateState();
|
|
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
|
|
const stored = await storeAndStripChallenge(cache, req, state, 'saml');
|
|
if (!stored) {
|
|
return res.redirect(
|
|
`${getAdminPanelUrl()}/auth/saml/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
|
|
);
|
|
}
|
|
|
|
return passport.authenticate('samlAdmin', {
|
|
session: false,
|
|
additionalParams: { RelayState: state },
|
|
})(req, res, next);
|
|
});
|
|
|
|
router.post(
|
|
'/oauth/saml/callback',
|
|
(req, res, next) => {
|
|
req.oauthState = typeof req.body.RelayState === 'string' ? req.body.RelayState : undefined;
|
|
next();
|
|
},
|
|
passport.authenticate('samlAdmin', {
|
|
failureRedirect: `${getAdminPanelUrl()}/auth/saml/callback?error=auth_failed&error_description=Authentication+failed`,
|
|
failureMessage: true,
|
|
session: false,
|
|
}),
|
|
tenantContextMiddleware,
|
|
retrievePkceChallenge('saml'),
|
|
requireAdminAccess,
|
|
setBalanceConfig,
|
|
middleware.checkDomainAllowed,
|
|
createOAuthHandler(`${getAdminPanelUrl()}/auth/saml/callback`),
|
|
);
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* 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`,
|
|
);
|
|
}
|
|
|
|
return passport.authenticate('googleAdmin', {
|
|
scope: ['openid', 'profile', 'email'],
|
|
session: false,
|
|
state,
|
|
})(req, res, next);
|
|
});
|
|
|
|
router.get(
|
|
'/oauth/google/callback',
|
|
(req, res, next) => {
|
|
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
|
|
next();
|
|
},
|
|
passport.authenticate('googleAdmin', {
|
|
failureRedirect: `${getAdminPanelUrl()}/auth/google/callback?error=auth_failed&error_description=Authentication+failed`,
|
|
failureMessage: true,
|
|
session: false,
|
|
}),
|
|
tenantContextMiddleware,
|
|
retrievePkceChallenge('google'),
|
|
requireAdminAccess,
|
|
setBalanceConfig,
|
|
middleware.checkDomainAllowed,
|
|
createOAuthHandler(`${getAdminPanelUrl()}/auth/google/callback`),
|
|
);
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* 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`,
|
|
);
|
|
}
|
|
|
|
return passport.authenticate('githubAdmin', {
|
|
scope: ['user:email', 'read:user'],
|
|
session: false,
|
|
state,
|
|
})(req, res, next);
|
|
});
|
|
|
|
router.get(
|
|
'/oauth/github/callback',
|
|
(req, res, next) => {
|
|
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
|
|
next();
|
|
},
|
|
passport.authenticate('githubAdmin', {
|
|
failureRedirect: `${getAdminPanelUrl()}/auth/github/callback?error=auth_failed&error_description=Authentication+failed`,
|
|
failureMessage: true,
|
|
session: false,
|
|
}),
|
|
tenantContextMiddleware,
|
|
retrievePkceChallenge('github'),
|
|
requireAdminAccess,
|
|
setBalanceConfig,
|
|
middleware.checkDomainAllowed,
|
|
createOAuthHandler(`${getAdminPanelUrl()}/auth/github/callback`),
|
|
);
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* 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`,
|
|
);
|
|
}
|
|
|
|
return passport.authenticate('discordAdmin', {
|
|
scope: ['identify', 'email'],
|
|
session: false,
|
|
state,
|
|
})(req, res, next);
|
|
});
|
|
|
|
router.get(
|
|
'/oauth/discord/callback',
|
|
(req, res, next) => {
|
|
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
|
|
next();
|
|
},
|
|
passport.authenticate('discordAdmin', {
|
|
failureRedirect: `${getAdminPanelUrl()}/auth/discord/callback?error=auth_failed&error_description=Authentication+failed`,
|
|
failureMessage: true,
|
|
session: false,
|
|
}),
|
|
tenantContextMiddleware,
|
|
retrievePkceChallenge('discord'),
|
|
requireAdminAccess,
|
|
setBalanceConfig,
|
|
middleware.checkDomainAllowed,
|
|
createOAuthHandler(`${getAdminPanelUrl()}/auth/discord/callback`),
|
|
);
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* 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`,
|
|
);
|
|
}
|
|
|
|
return passport.authenticate('facebookAdmin', {
|
|
scope: ['public_profile'],
|
|
session: false,
|
|
state,
|
|
})(req, res, next);
|
|
});
|
|
|
|
router.get(
|
|
'/oauth/facebook/callback',
|
|
(req, res, next) => {
|
|
req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
|
|
next();
|
|
},
|
|
passport.authenticate('facebookAdmin', {
|
|
failureRedirect: `${getAdminPanelUrl()}/auth/facebook/callback?error=auth_failed&error_description=Authentication+failed`,
|
|
failureMessage: true,
|
|
session: false,
|
|
}),
|
|
tenantContextMiddleware,
|
|
retrievePkceChallenge('facebook'),
|
|
requireAdminAccess,
|
|
setBalanceConfig,
|
|
middleware.checkDomainAllowed,
|
|
createOAuthHandler(`${getAdminPanelUrl()}/auth/facebook/callback`),
|
|
);
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* Apple Admin Routes (POST callback)
|
|
* ────────────────────────────────────────────── */
|
|
|
|
router.get('/oauth/apple', async (req, res, next) => {
|
|
const state = generateState();
|
|
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
|
|
const stored = await storeAndStripChallenge(cache, req, state, 'apple');
|
|
if (!stored) {
|
|
return res.redirect(
|
|
`${getAdminPanelUrl()}/auth/apple/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
|
|
);
|
|
}
|
|
|
|
return passport.authenticate('appleAdmin', {
|
|
session: false,
|
|
state,
|
|
})(req, res, next);
|
|
});
|
|
|
|
router.post(
|
|
'/oauth/apple/callback',
|
|
(req, res, next) => {
|
|
req.oauthState = typeof req.body.state === 'string' ? req.body.state : undefined;
|
|
next();
|
|
},
|
|
passport.authenticate('appleAdmin', {
|
|
failureRedirect: `${getAdminPanelUrl()}/auth/apple/callback?error=auth_failed&error_description=Authentication+failed`,
|
|
failureMessage: true,
|
|
session: false,
|
|
}),
|
|
tenantContextMiddleware,
|
|
retrievePkceChallenge('apple'),
|
|
requireAdminAccess,
|
|
setBalanceConfig,
|
|
middleware.checkDomainAllowed,
|
|
createOAuthHandler(`${getAdminPanelUrl()}/auth/apple/callback`),
|
|
);
|
|
|
|
/** Regex pattern for valid exchange codes: 64 hex characters */
|
|
const EXCHANGE_CODE_PATTERN = /^[a-f0-9]{64}$/;
|
|
|
|
/**
|
|
* Exchange OAuth authorization code for tokens.
|
|
* This endpoint is called server-to-server by the admin panel.
|
|
* The code is one-time-use and expires in 30 seconds.
|
|
*
|
|
* POST /api/admin/oauth/exchange
|
|
* Body: { code: string, code_verifier?: string }
|
|
* Response: { token: string, refreshToken: string, user: object }
|
|
*/
|
|
router.post('/oauth/exchange', middleware.loginLimiter, async (req, res) => {
|
|
try {
|
|
const { code, code_verifier: codeVerifier } = req.body;
|
|
|
|
if (!code) {
|
|
logger.warn('[admin/oauth/exchange] Missing authorization code');
|
|
return res.status(400).json({
|
|
error: 'Missing authorization code',
|
|
error_code: 'MISSING_CODE',
|
|
});
|
|
}
|
|
|
|
if (typeof code !== 'string' || !EXCHANGE_CODE_PATTERN.test(code)) {
|
|
logger.warn('[admin/oauth/exchange] Invalid authorization code format');
|
|
return res.status(400).json({
|
|
error: 'Invalid authorization code format',
|
|
error_code: 'INVALID_CODE_FORMAT',
|
|
});
|
|
}
|
|
|
|
if (
|
|
codeVerifier !== undefined &&
|
|
(typeof codeVerifier !== 'string' || codeVerifier.length < 1 || codeVerifier.length > 512)
|
|
) {
|
|
logger.warn('[admin/oauth/exchange] Invalid code_verifier format');
|
|
return res.status(400).json({
|
|
error: 'Invalid code_verifier',
|
|
error_code: 'INVALID_VERIFIER',
|
|
});
|
|
}
|
|
|
|
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
|
|
const requestOrigin = resolveRequestOrigin(req);
|
|
const result = await exchangeAdminCode(cache, code, requestOrigin, codeVerifier);
|
|
|
|
if (!result) {
|
|
return res.status(401).json({
|
|
error: 'Invalid or expired authorization code',
|
|
error_code: 'INVALID_OR_EXPIRED_CODE',
|
|
});
|
|
}
|
|
|
|
res.json(result);
|
|
} catch (error) {
|
|
logger.error('[admin/oauth/exchange] Error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
error_code: 'INTERNAL_ERROR',
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Admin-panel-shaped token refresh.
|
|
*
|
|
* The standard `/api/auth/refresh` controller reads the refresh token from
|
|
* cookies, which a cross-origin admin panel can't set. This endpoint accepts
|
|
* the refresh token in the request body, exchanges it at the IdP, mints a
|
|
* fresh LibreChat JWT, and returns the same response shape as
|
|
* `/api/admin/oauth/exchange`.
|
|
*
|
|
* POST /api/admin/oauth/refresh
|
|
* Body: { refresh_token: string, user_id?: string }
|
|
* Response: { token: string, refreshToken?: string, user: object, expiresAt: number }
|
|
*
|
|
* Errors (all responses are `{ error: string, error_code: string }`):
|
|
* 400 MISSING_REFRESH_TOKEN — refresh_token absent or empty
|
|
* 401 REFRESH_FAILED — IdP rejected the refresh grant
|
|
* 401 USER_NOT_FOUND — no LibreChat user matches the refreshed sub
|
|
* 401 USER_ID_MISMATCH — supplied user_id resolves to a user with a different openidId
|
|
* 401 ISSUER_MISMATCH — refreshed tokenset was issued by an unexpected issuer
|
|
* 401 TENANT_MISMATCH — resolved user belongs to a different tenant than the request
|
|
* 403 FORBIDDEN — resolved user no longer holds ACCESS_ADMIN
|
|
* 403 TOKEN_REUSE_DISABLED — OPENID_REUSE_TOKENS is not enabled on the server
|
|
* 502 IDP_INCOMPLETE — IdP returned a tokenset missing access_token
|
|
* 502 CLAIMS_INCOMPLETE — IdP tokenset has no readable claims or no sub
|
|
* 503 OPENID_NOT_CONFIGURED — OpenID is not configured on this server
|
|
* 500 INTERNAL_ERROR — anything else (logged server-side)
|
|
*/
|
|
router.post(
|
|
'/oauth/refresh',
|
|
middleware.loginLimiter,
|
|
preAuthTenantMiddleware,
|
|
async (req, res) => {
|
|
try {
|
|
const { refresh_token: refreshToken, user_id: userId } = req.body ?? {};
|
|
if (typeof refreshToken !== 'string' || refreshToken.length === 0) {
|
|
return res.status(400).json({
|
|
error: 'Missing refresh_token',
|
|
error_code: 'MISSING_REFRESH_TOKEN',
|
|
});
|
|
}
|
|
|
|
if (!isEnabled(process.env.OPENID_REUSE_TOKENS)) {
|
|
return res.status(403).json({
|
|
error: 'OpenID token reuse is not enabled',
|
|
error_code: 'TOKEN_REUSE_DISABLED',
|
|
});
|
|
}
|
|
|
|
let openIdConfig;
|
|
try {
|
|
openIdConfig = getOpenIdConfig();
|
|
} catch {
|
|
return res.status(503).json({
|
|
error: 'OpenID is not configured',
|
|
error_code: 'OPENID_NOT_CONFIGURED',
|
|
});
|
|
}
|
|
|
|
const refreshParams = buildOpenIDRefreshParams();
|
|
logger.debug('[admin/oauth/refresh] OpenID refresh params', {
|
|
has_scope: Boolean(process.env.OPENID_SCOPE),
|
|
has_refresh_audience: Boolean(process.env.OPENID_REFRESH_AUDIENCE),
|
|
});
|
|
let tokenset;
|
|
try {
|
|
tokenset = await openIdClient.refreshTokenGrant(openIdConfig, refreshToken, refreshParams);
|
|
logger.debug('[admin/oauth/refresh] OpenID refresh succeeded', {
|
|
has_access_token: Boolean(tokenset.access_token),
|
|
has_id_token: Boolean(tokenset.id_token),
|
|
has_refresh_token: Boolean(tokenset.refresh_token),
|
|
expires_in: tokenset.expires_in,
|
|
});
|
|
} catch (err) {
|
|
logger.warn('[admin/oauth/refresh] IdP refresh grant failed', {
|
|
code: err?.code,
|
|
name: err?.name,
|
|
});
|
|
return res.status(401).json({
|
|
error: 'Refresh failed',
|
|
error_code: 'REFRESH_FAILED',
|
|
});
|
|
}
|
|
|
|
const sessionExpiry = Number(process.env.SESSION_EXPIRY) || DEFAULT_SESSION_EXPIRY;
|
|
const expectedIssuer = openIdConfig.serverMetadata?.()?.issuer;
|
|
|
|
try {
|
|
const result = await applyAdminRefresh(
|
|
tokenset,
|
|
{
|
|
findUsers,
|
|
getUserById,
|
|
canAccessAdmin: async (user) => {
|
|
try {
|
|
return await hasCapability(
|
|
{
|
|
id: user.id ?? user._id?.toString(),
|
|
role: user.role ?? '',
|
|
tenantId: user.tenantId,
|
|
},
|
|
SystemCapabilities.ACCESS_ADMIN,
|
|
);
|
|
} catch (err) {
|
|
logger.warn(
|
|
`[admin/oauth/refresh] capability check failed, denying: ${err?.message}`,
|
|
);
|
|
return false;
|
|
}
|
|
},
|
|
mintToken: async (user) => ({
|
|
token: await generateToken(user, sessionExpiry),
|
|
expiresAt: Date.now() + sessionExpiry,
|
|
}),
|
|
},
|
|
{
|
|
userId: typeof userId === 'string' && userId.length > 0 ? userId : undefined,
|
|
previousRefreshToken: refreshToken,
|
|
expectedIssuer,
|
|
tenantId: getTenantId(),
|
|
},
|
|
);
|
|
return res.json(result);
|
|
} catch (err) {
|
|
if (err instanceof AdminRefreshError) {
|
|
return res.status(err.status).json({ error: err.message, error_code: err.code });
|
|
}
|
|
throw err;
|
|
}
|
|
} catch (error) {
|
|
logger.error('[admin/oauth/refresh] Error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
error_code: 'INTERNAL_ERROR',
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
module.exports = router;
|