mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-09 12:08:50 +01:00
🍪 refactor: Move OpenID Tokens from Cookies to Server-Side Sessions (#11236)
* refactor: OpenID token handling by storing tokens in session to reduce cookie size * refactor: Improve OpenID user identification logic in logout controller * refactor: Enhance OpenID logout flow by adding post-logout redirect URI * refactor: Update logout process to clear additional OpenID user ID cookie
This commit is contained in:
parent
3b41e392ba
commit
348b4a4a32
8 changed files with 105 additions and 38 deletions
|
|
@ -525,6 +525,8 @@ OPENID_ON_BEHALF_FLOW_FOR_USERINFO_REQUIRED=
|
|||
OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE="user.read" # example for Scope Needed for Microsoft Graph API
|
||||
# Set to true to use the OpenID Connect end session endpoint for logout
|
||||
OPENID_USE_END_SESSION_ENDPOINT=
|
||||
# URL to redirect to after OpenID logout (defaults to ${DOMAIN_CLIENT}/login)
|
||||
OPENID_POST_LOGOUT_REDIRECT_URI=
|
||||
|
||||
#========================#
|
||||
# SharePoint Integration #
|
||||
|
|
|
|||
9
api/cache/banViolation.js
vendored
9
api/cache/banViolation.js
vendored
|
|
@ -47,7 +47,16 @@ const banViolation = async (req, res, errorMessage) => {
|
|||
}
|
||||
|
||||
await deleteAllUserSessions({ userId: user_id });
|
||||
|
||||
/** Clear OpenID session tokens if present */
|
||||
if (req.session?.openidTokens) {
|
||||
delete req.session.openidTokens;
|
||||
}
|
||||
|
||||
res.clearCookie('refreshToken');
|
||||
res.clearCookie('openid_access_token');
|
||||
res.clearCookie('openid_user_id');
|
||||
res.clearCookie('token_provider');
|
||||
|
||||
const banLogs = getLogStores(ViolationTypes.BAN);
|
||||
const duration = errorMessage.duration || banLogs.opts.ttl;
|
||||
|
|
|
|||
|
|
@ -66,14 +66,17 @@ const resetPasswordController = async (req, res) => {
|
|||
};
|
||||
|
||||
const refreshController = async (req, res) => {
|
||||
const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null;
|
||||
const token_provider = req.headers.cookie
|
||||
? cookies.parse(req.headers.cookie).token_provider
|
||||
: null;
|
||||
if (!refreshToken) {
|
||||
return res.status(200).send('Refresh token not provided');
|
||||
}
|
||||
if (token_provider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS) === true) {
|
||||
const parsedCookies = req.headers.cookie ? cookies.parse(req.headers.cookie) : {};
|
||||
const token_provider = parsedCookies.token_provider;
|
||||
|
||||
if (token_provider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS)) {
|
||||
/** For OpenID users, read refresh token from session to avoid large cookie issues */
|
||||
const refreshToken = req.session?.openidTokens?.refreshToken || parsedCookies.refreshToken;
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(200).send('Refresh token not provided');
|
||||
}
|
||||
|
||||
try {
|
||||
const openIdConfig = getOpenIdConfig();
|
||||
const tokenset = await openIdClient.refreshTokenGrant(openIdConfig, refreshToken);
|
||||
|
|
@ -110,7 +113,7 @@ const refreshController = async (req, res) => {
|
|||
);
|
||||
}
|
||||
|
||||
const token = setOpenIDAuthTokens(tokenset, res, user._id.toString(), refreshToken);
|
||||
const token = setOpenIDAuthTokens(tokenset, req, res, user._id.toString(), refreshToken);
|
||||
|
||||
user.federatedTokens = {
|
||||
access_token: tokenset.access_token,
|
||||
|
|
@ -125,6 +128,13 @@ const refreshController = async (req, res) => {
|
|||
return res.status(403).send('Invalid OpenID refresh token');
|
||||
}
|
||||
}
|
||||
|
||||
/** For non-OpenID users, read refresh token from cookies */
|
||||
const refreshToken = parsedCookies.refreshToken;
|
||||
if (!refreshToken) {
|
||||
return res.status(200).send('Refresh token not provided');
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||
const user = await getUserById(payload.id, '-password -__v -totpSecret -backupCodes');
|
||||
|
|
|
|||
|
|
@ -5,15 +5,28 @@ const { logoutUser } = require('~/server/services/AuthService');
|
|||
const { getOpenIdConfig } = require('~/strategies');
|
||||
|
||||
const logoutController = async (req, res) => {
|
||||
const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null;
|
||||
const parsedCookies = req.headers.cookie ? cookies.parse(req.headers.cookie) : {};
|
||||
const isOpenIdUser = req.user?.openidId != null && req.user?.provider === 'openid';
|
||||
|
||||
/** For OpenID users, read refresh token from session; for others, use cookie */
|
||||
let refreshToken;
|
||||
if (isOpenIdUser && req.session?.openidTokens) {
|
||||
refreshToken = req.session.openidTokens.refreshToken;
|
||||
delete req.session.openidTokens;
|
||||
}
|
||||
refreshToken = refreshToken || parsedCookies.refreshToken;
|
||||
|
||||
try {
|
||||
const logout = await logoutUser(req, refreshToken);
|
||||
const { status, message } = logout;
|
||||
|
||||
res.clearCookie('refreshToken');
|
||||
res.clearCookie('openid_access_token');
|
||||
res.clearCookie('openid_user_id');
|
||||
res.clearCookie('token_provider');
|
||||
const response = { message };
|
||||
if (
|
||||
req.user.openidId != null &&
|
||||
isOpenIdUser &&
|
||||
isEnabled(process.env.OPENID_USE_END_SESSION_ENDPOINT) &&
|
||||
process.env.OPENID_ISSUER
|
||||
) {
|
||||
|
|
@ -27,7 +40,12 @@ const logoutController = async (req, res) => {
|
|||
? openIdConfig.serverMetadata().end_session_endpoint
|
||||
: null;
|
||||
if (endSessionEndpoint) {
|
||||
response.redirect = endSessionEndpoint;
|
||||
const endSessionUrl = new URL(endSessionEndpoint);
|
||||
/** Redirect back to app's login page after IdP logout */
|
||||
const postLogoutRedirectUri =
|
||||
process.env.OPENID_POST_LOGOUT_REDIRECT_URI || `${process.env.DOMAIN_CLIENT}/login`;
|
||||
endSessionUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
|
||||
response.redirect = endSessionUrl.toString();
|
||||
} else {
|
||||
logger.warn(
|
||||
'[logoutController] end_session_endpoint not found in OpenID issuer metadata. Please verify that the issuer is correct.',
|
||||
|
|
|
|||
|
|
@ -68,17 +68,11 @@ function createValidateImageRequest(secureImageLinks) {
|
|||
}
|
||||
|
||||
const parsedCookies = cookies.parse(cookieHeader);
|
||||
const refreshToken = parsedCookies.refreshToken;
|
||||
|
||||
if (!refreshToken) {
|
||||
logger.warn('[validateImageRequest] Token not provided');
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
|
||||
const tokenProvider = parsedCookies.token_provider;
|
||||
let userIdForPath;
|
||||
|
||||
if (tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS)) {
|
||||
/** For OpenID users with OPENID_REUSE_TOKENS, use openid_user_id cookie */
|
||||
const openidUserId = parsedCookies.openid_user_id;
|
||||
if (!openidUserId) {
|
||||
logger.warn('[validateImageRequest] No OpenID user ID cookie found');
|
||||
|
|
@ -92,6 +86,17 @@ function createValidateImageRequest(secureImageLinks) {
|
|||
}
|
||||
userIdForPath = validationResult.userId;
|
||||
} else {
|
||||
/**
|
||||
* For non-OpenID users (or OpenID without REUSE_TOKENS), use refreshToken from cookies.
|
||||
* These users authenticate via setAuthTokens() which stores refreshToken in cookies.
|
||||
*/
|
||||
const refreshToken = parsedCookies.refreshToken;
|
||||
|
||||
if (!refreshToken) {
|
||||
logger.warn('[validateImageRequest] Token not provided');
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
|
||||
const validationResult = validateToken(refreshToken);
|
||||
if (!validationResult.valid) {
|
||||
logger.warn(`[validateImageRequest] ${validationResult.error}`);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const oauthHandler = async (req, res, next) => {
|
|||
isEnabled(process.env.OPENID_REUSE_TOKENS) === true
|
||||
) {
|
||||
await syncUserEntraGroupMemberships(req.user, req.user.tokenset.access_token);
|
||||
setOpenIDAuthTokens(req.user.tokenset, res, req.user._id.toString());
|
||||
setOpenIDAuthTokens(req.user.tokenset, req, res, req.user._id.toString());
|
||||
} else {
|
||||
await setAuthTokens(req.user._id, res);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -411,14 +411,17 @@ const setAuthTokens = async (userId, res, _session = null) => {
|
|||
/**
|
||||
* @function setOpenIDAuthTokens
|
||||
* Set OpenID Authentication Tokens
|
||||
* //type tokenset from openid-client
|
||||
* Stores tokens server-side in express-session to avoid large cookie sizes
|
||||
* that can exceed HTTP/2 header limits (especially for users with many group memberships).
|
||||
*
|
||||
* @param {import('openid-client').TokenEndpointResponse & import('openid-client').TokenEndpointResponseHelpers} tokenset
|
||||
* - The tokenset object containing access and refresh tokens
|
||||
* @param {Object} req - request object (for session access)
|
||||
* @param {Object} res - response object
|
||||
* @param {string} [userId] - Optional MongoDB user ID for image path validation
|
||||
* @returns {String} - access token
|
||||
*/
|
||||
const setOpenIDAuthTokens = (tokenset, res, userId, existingRefreshToken) => {
|
||||
const setOpenIDAuthTokens = (tokenset, req, res, userId, existingRefreshToken) => {
|
||||
try {
|
||||
if (!tokenset) {
|
||||
logger.error('[setOpenIDAuthTokens] No tokenset found in request');
|
||||
|
|
@ -445,18 +448,30 @@ const setOpenIDAuthTokens = (tokenset, res, userId, existingRefreshToken) => {
|
|||
return;
|
||||
}
|
||||
|
||||
res.cookie('refreshToken', refreshToken, {
|
||||
expires: expirationDate,
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
res.cookie('openid_access_token', tokenset.access_token, {
|
||||
expires: expirationDate,
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
/** Store tokens server-side in session to avoid large cookies */
|
||||
if (req.session) {
|
||||
req.session.openidTokens = {
|
||||
accessToken: tokenset.access_token,
|
||||
refreshToken: refreshToken,
|
||||
expiresAt: expirationDate.getTime(),
|
||||
};
|
||||
} else {
|
||||
logger.warn('[setOpenIDAuthTokens] No session available, falling back to cookies');
|
||||
res.cookie('refreshToken', refreshToken, {
|
||||
expires: expirationDate,
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
res.cookie('openid_access_token', tokenset.access_token, {
|
||||
expires: expirationDate,
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
}
|
||||
|
||||
/** Small cookie to indicate token provider (required for auth middleware) */
|
||||
res.cookie('token_provider', 'openid', {
|
||||
expires: expirationDate,
|
||||
httpOnly: true,
|
||||
|
|
|
|||
|
|
@ -81,10 +81,18 @@ const openIdJwtLogin = (openIdConfig) => {
|
|||
await updateUser(user.id, updateData);
|
||||
}
|
||||
|
||||
const cookieHeader = req.headers.cookie;
|
||||
const parsedCookies = cookieHeader ? cookies.parse(cookieHeader) : {};
|
||||
const accessToken = parsedCookies.openid_access_token;
|
||||
const refreshToken = parsedCookies.refreshToken;
|
||||
/** Read tokens from session (server-side) to avoid large cookie issues */
|
||||
const sessionTokens = req.session?.openidTokens;
|
||||
let accessToken = sessionTokens?.accessToken;
|
||||
let refreshToken = sessionTokens?.refreshToken;
|
||||
|
||||
/** Fallback to cookies for backward compatibility */
|
||||
if (!accessToken || !refreshToken) {
|
||||
const cookieHeader = req.headers.cookie;
|
||||
const parsedCookies = cookieHeader ? cookies.parse(cookieHeader) : {};
|
||||
accessToken = accessToken || parsedCookies.openid_access_token;
|
||||
refreshToken = refreshToken || parsedCookies.refreshToken;
|
||||
}
|
||||
|
||||
user.federatedTokens = {
|
||||
access_token: accessToken || rawToken,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue