🍪 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:
Danny Avila 2026-01-06 15:22:10 -05:00 committed by GitHub
parent 3b41e392ba
commit 348b4a4a32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 105 additions and 38 deletions

View file

@ -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 #

View file

@ -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;

View file

@ -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');

View file

@ -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.',

View file

@ -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}`);

View file

@ -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);
}

View file

@ -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,

View file

@ -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,