mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🛡️ fix: Harden OpenID Session Token Reuse (#13086)
* fix: Harden OpenID Session Token Reuse * fix: Preserve OpenID Session Token On Forced Refresh * fix: Gate Preserved OpenID Id Token By Expiry * test: Cover OpenID Id Token Expiry Buffer
This commit is contained in:
parent
17a08224e1
commit
7b9a57a467
4 changed files with 457 additions and 31 deletions
|
|
@ -27,6 +27,9 @@ const { getGraphApiToken } = require('~/server/services/GraphTokenService');
|
|||
const { getOpenIdConfig, getOpenIdEmail } = require('~/strategies');
|
||||
|
||||
const AUTH_REFRESH_USER_PROJECTION = '-password -__v -totpSecret -backupCodes -federatedTokens';
|
||||
const OPENID_REUSE_EXPIRY_BUFFER_SECONDS = 30;
|
||||
/** Mirrors the default SESSION_EXPIRY to bound IdP revocation lag for session-token reuse. */
|
||||
const OPENID_REUSE_MAX_SESSION_AGE_MS = 15 * 60 * 1000;
|
||||
|
||||
const registrationController = async (req, res) => {
|
||||
try {
|
||||
|
|
@ -68,7 +71,19 @@ const getValidOpenIDReuseUserId = (parsedCookies) => {
|
|||
}
|
||||
};
|
||||
|
||||
const isRecentOpenIDSessionRefresh = (openidTokens) => {
|
||||
const lastRefreshedAt = Number(openidTokens?.lastRefreshedAt);
|
||||
const elapsed = Date.now() - lastRefreshedAt;
|
||||
return (
|
||||
Number.isFinite(lastRefreshedAt) && elapsed >= 0 && elapsed <= OPENID_REUSE_MAX_SESSION_AGE_MS
|
||||
);
|
||||
};
|
||||
|
||||
const getReusableOpenIDSessionToken = (openidTokens) => {
|
||||
if (!isRecentOpenIDSessionRefresh(openidTokens)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
{ token: openidTokens?.idToken, type: 'id_token' },
|
||||
{ token: openidTokens?.accessToken, type: 'access_token' },
|
||||
|
|
@ -79,8 +94,13 @@ const getReusableOpenIDSessionToken = (openidTokens) => {
|
|||
if (!candidate.token) {
|
||||
continue;
|
||||
}
|
||||
/** Decode only: tokens are from the trusted server-side session; expiry gates reuse. */
|
||||
const decoded = jwt.decode(candidate.token);
|
||||
if (decoded && typeof decoded === 'object' && decoded.exp > now) {
|
||||
if (
|
||||
decoded &&
|
||||
typeof decoded === 'object' &&
|
||||
decoded.exp > now + OPENID_REUSE_EXPIRY_BUFFER_SECONDS
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
|
@ -134,6 +154,11 @@ const refreshController = async (req, res) => {
|
|||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Reuse skips an IdP refresh only for recently-refreshed server-side tokens.
|
||||
* Stale, missing, or near-expiry tokens fall through to refreshTokenGrant so
|
||||
* upstream revocations and cookie/session extension are checked regularly.
|
||||
*/
|
||||
const reusableSessionToken = getReusableOpenIDSessionToken(req.session?.openidTokens);
|
||||
const reuseUserId = reusableSessionToken ? getValidOpenIDReuseUserId(parsedCookies) : null;
|
||||
if (reuseUserId) {
|
||||
|
|
@ -144,7 +169,6 @@ const refreshController = async (req, res) => {
|
|||
token_type: reusableSessionToken.type,
|
||||
has_id_token: Boolean(req.session?.openidTokens?.idToken),
|
||||
has_access_token: Boolean(req.session?.openidTokens?.accessToken),
|
||||
cloudfront_cookies_attempted: true,
|
||||
cloudfront_cookies_set: cloudFrontCookiesSet,
|
||||
});
|
||||
return res.status(200).send({
|
||||
|
|
@ -237,7 +261,7 @@ const refreshController = async (req, res) => {
|
|||
|
||||
if (process.env.NODE_ENV === 'CI') {
|
||||
const token = await setAuthTokens(userId, res, null, req);
|
||||
return res.status(200).send({ token, user });
|
||||
return res.status(200).send({ token, user: sanitizeUserForAuthResponse(user) });
|
||||
}
|
||||
|
||||
/** Session with the hashed refresh token */
|
||||
|
|
@ -252,7 +276,7 @@ const refreshController = async (req, res) => {
|
|||
if (session && session.expiration > new Date()) {
|
||||
const token = await setAuthTokens(userId, res, session, req);
|
||||
|
||||
res.status(200).send({ token, user });
|
||||
res.status(200).send({ token, user: sanitizeUserForAuthResponse(user) });
|
||||
} else if (req?.query?.retry) {
|
||||
// Retrying from a refresh token request that failed (401)
|
||||
res.status(403).send('No session found');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue