LibreChat/api/strategies/openIdJwtStrategy.js
Danny Avila 98704f28c1
🌐 fix: Centralize Outbound Proxy Handling (#13726)
* fix: centralize outbound proxy handling

* chore: sort proxy imports

* test: update proxy helper mocks

* fix: honor proxy bypasses consistently

* fix: support http axios proxy targets
2026-06-14 10:47:49 -04:00

176 lines
6.1 KiB
JavaScript

const cookies = require('cookie');
const jwksRsa = require('jwks-rsa');
const { logger } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const {
isEnabled,
findOpenIDUser,
getOpenIdEmail,
getOpenIdIssuer,
normalizeOpenIdIssuer,
getHttpsProxyAgent,
math,
} = require('@librechat/api');
const { updateUser, findUser } = require('~/models');
const getOpenIdJwtAudience = () => {
const parsedAudience = (process.env.OPENID_AUDIENCE ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const audiences = [process.env.OPENID_CLIENT_ID, ...parsedAudience].filter(Boolean);
const uniqueAudiences = [...new Set(audiences)];
return uniqueAudiences.length > 1 ? uniqueAudiences : uniqueAudiences[0];
};
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const issuerMatchesTemplate = (expectedIssuer, actualIssuer) => {
if (!expectedIssuer.includes('{tenantid}')) {
return false;
}
const escapedTemplate = expectedIssuer.split('{tenantid}').map(escapeRegExp).join('[^/]+');
return new RegExp(`^${escapedTemplate}$`).test(actualIssuer);
};
const isOpenIdIssuerAllowed = (payload, openIdConfig) => {
const actualIssuer = normalizeOpenIdIssuer(payload?.iss);
const expectedIssuer = normalizeOpenIdIssuer(openIdConfig.serverMetadata().issuer);
if (!actualIssuer || !expectedIssuer) {
return false;
}
return actualIssuer === expectedIssuer || issuerMatchesTemplate(expectedIssuer, actualIssuer);
};
/**
* @function openIdJwtLogin
* @param {import('openid-client').Configuration} openIdConfig - Configuration object for the JWT strategy.
* @returns {JwtStrategy}
* @description This function creates a JWT strategy for OpenID authentication.
* It uses the jwks-rsa library to retrieve the signing key from a JWKS endpoint.
* The strategy extracts the JWT from the Authorization header as a Bearer token.
* The JWT is then verified using the signing key, and the user is retrieved from the database.
*
* Includes email fallback mechanism:
* 1. Primary lookup: Search user by openidId (sub claim)
* 2. Fallback lookup: If not found, search by email claim
* 3. User migration: If found by email without openidId, migrate the user by adding openidId
* 4. Provider validation: Ensures users registered with other providers cannot use OpenID
*
* This enables seamless migration for existing users when SharePoint integration is enabled.
*/
const openIdJwtLogin = (openIdConfig) => {
let jwksRsaOptions = {
cache: process.env.OPENID_JWKS_URL_CACHE_ENABLED
? isEnabled(process.env.OPENID_JWKS_URL_CACHE_ENABLED)
: true,
cacheMaxAge: math(process.env.OPENID_JWKS_URL_CACHE_TIME, 60000),
jwksUri: openIdConfig.serverMetadata().jwks_uri,
};
const requestAgent = getHttpsProxyAgent(jwksRsaOptions.jwksUri);
if (requestAgent) {
jwksRsaOptions.requestAgent = requestAgent;
}
return new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKeyProvider: jwksRsa.passportJwtSecret(jwksRsaOptions),
audience: getOpenIdJwtAudience(),
passReqToCallback: true,
},
/**
* @param {import('@librechat/api').ServerRequest} req
* @param {import('openid-client').IDToken} payload
* @param {import('passport-jwt').VerifyCallback} done
*/
async (req, payload, done) => {
try {
if (!isOpenIdIssuerAllowed(payload, openIdConfig)) {
done(null, false, { message: 'Invalid issuer' });
return;
}
const authHeader = req.headers.authorization;
const rawToken = authHeader?.replace('Bearer ', '');
const openidIssuer = getOpenIdIssuer(payload, openIdConfig);
const { user, error, migration } = await findOpenIDUser({
findUser,
email: payload ? getOpenIdEmail(payload) : undefined,
openidId: payload?.sub,
openidIssuer,
idOnTheSource: payload?.oid,
strategyName: 'openIdJwtLogin',
});
if (error) {
done(null, false, { message: error });
return;
}
if (user) {
user.id = user._id.toString();
const updateData = {};
if (migration) {
updateData.provider = 'openid';
updateData.openidId = payload?.sub;
if (openidIssuer) {
updateData.openidIssuer = openidIssuer;
}
}
if (!user.role) {
user.role = SystemRoles.USER;
updateData.role = user.role;
}
if (Object.keys(updateData).length > 0) {
await updateUser(user.id, updateData);
}
/** Read tokens from session (server-side) to avoid large cookie issues */
const sessionTokens = req.session?.openidTokens;
let accessToken = sessionTokens?.accessToken;
let idToken = sessionTokens?.idToken;
let refreshToken = sessionTokens?.refreshToken;
/** Fallback to cookies for backward compatibility */
if (!accessToken || !refreshToken || !idToken) {
const cookieHeader = req.headers.cookie;
const parsedCookies = cookieHeader ? cookies.parse(cookieHeader) : {};
accessToken = accessToken || parsedCookies.openid_access_token;
idToken = idToken || parsedCookies.openid_id_token;
refreshToken = refreshToken || parsedCookies.refreshToken;
}
user.federatedTokens = {
access_token: accessToken || rawToken,
id_token: idToken,
refresh_token: refreshToken,
expires_at: payload.exp,
};
done(null, user);
} else {
logger.warn(
'[openIdJwtLogin] openId JwtStrategy => no user found with the sub claims: ' +
payload?.sub +
(payload?.email ? ' or email: ' + payload.email : ''),
);
done(null, false);
}
} catch (err) {
done(err, false);
}
},
);
};
module.exports = openIdJwtLogin;