mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
🍪 fix: Refresh CloudFront Cookies On Auth Refresh (#13083)
* fix: Refresh CloudFront Cookies On Auth Refresh * fix: Exclude Federated Tokens From Refresh Lookup
This commit is contained in:
parent
929082387f
commit
17a08224e1
4 changed files with 465 additions and 43 deletions
|
|
@ -11,6 +11,7 @@ const {
|
|||
const {
|
||||
requestPasswordReset,
|
||||
setOpenIDAuthTokens,
|
||||
setCloudFrontAuthCookies,
|
||||
resetPassword,
|
||||
setAuthTokens,
|
||||
registerUser,
|
||||
|
|
@ -25,6 +26,8 @@ const {
|
|||
const { getGraphApiToken } = require('~/server/services/GraphTokenService');
|
||||
const { getOpenIdConfig, getOpenIdEmail } = require('~/strategies');
|
||||
|
||||
const AUTH_REFRESH_USER_PROJECTION = '-password -__v -totpSecret -backupCodes -federatedTokens';
|
||||
|
||||
const registrationController = async (req, res) => {
|
||||
try {
|
||||
const response = await registerUser(req.body);
|
||||
|
|
@ -36,6 +39,55 @@ const registrationController = async (req, res) => {
|
|||
}
|
||||
};
|
||||
|
||||
const sanitizeUserForAuthResponse = (user) => {
|
||||
const source = (typeof user?.toObject === 'function' ? user.toObject() : user) || {};
|
||||
const {
|
||||
password: _pw,
|
||||
__v: _v,
|
||||
totpSecret: _ts,
|
||||
backupCodes: _bc,
|
||||
federatedTokens: _ft,
|
||||
...safeUser
|
||||
} = source;
|
||||
return safeUser;
|
||||
};
|
||||
|
||||
const getValidOpenIDReuseUserId = (parsedCookies) => {
|
||||
const openidUserId = parsedCookies.openid_user_id;
|
||||
if (!openidUserId || !process.env.JWT_REFRESH_SECRET) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(openidUserId, process.env.JWT_REFRESH_SECRET);
|
||||
return typeof payload === 'object' && payload != null && typeof payload.id === 'string'
|
||||
? payload.id
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getReusableOpenIDSessionToken = (openidTokens) => {
|
||||
const candidates = [
|
||||
{ token: openidTokens?.idToken, type: 'id_token' },
|
||||
{ token: openidTokens?.accessToken, type: 'access_token' },
|
||||
];
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate.token) {
|
||||
continue;
|
||||
}
|
||||
const decoded = jwt.decode(candidate.token);
|
||||
if (decoded && typeof decoded === 'object' && decoded.exp > now) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const resetPasswordRequestController = async (req, res) => {
|
||||
try {
|
||||
const resetService = await requestPasswordReset(req);
|
||||
|
|
@ -82,6 +134,26 @@ const refreshController = async (req, res) => {
|
|||
}
|
||||
|
||||
try {
|
||||
const reusableSessionToken = getReusableOpenIDSessionToken(req.session?.openidTokens);
|
||||
const reuseUserId = reusableSessionToken ? getValidOpenIDReuseUserId(parsedCookies) : null;
|
||||
if (reuseUserId) {
|
||||
const user = await getUserById(reuseUserId, AUTH_REFRESH_USER_PROJECTION);
|
||||
if (user) {
|
||||
const cloudFrontCookiesSet = setCloudFrontAuthCookies(req, res, user);
|
||||
logger.debug('[refreshController] OpenID session token reused', {
|
||||
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({
|
||||
token: reusableSessionToken.token,
|
||||
user: sanitizeUserForAuthResponse(user),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const openIdConfig = getOpenIdConfig();
|
||||
const refreshParams = buildOpenIDRefreshParams();
|
||||
logger.debug('[refreshController] OpenID refresh params', {
|
||||
|
|
@ -141,8 +213,7 @@ const refreshController = async (req, res) => {
|
|||
tenantId: user.tenantId,
|
||||
});
|
||||
|
||||
const { password: _pw, __v: _v, totpSecret: _ts, backupCodes: _bc, ...safeUser } = user;
|
||||
return res.status(200).send({ token, user: safeUser });
|
||||
return res.status(200).send({ token, user: sanitizeUserForAuthResponse(user) });
|
||||
} catch (error) {
|
||||
logger.error('[refreshController] OpenID token refresh error', error);
|
||||
return res.status(403).send('Invalid OpenID refresh token');
|
||||
|
|
@ -157,7 +228,7 @@ const refreshController = async (req, res) => {
|
|||
|
||||
try {
|
||||
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||
const user = await getUserById(payload.id, '-password -__v -totpSecret -backupCodes');
|
||||
const user = await getUserById(payload.id, AUTH_REFRESH_USER_PROJECTION);
|
||||
if (!user) {
|
||||
return res.status(401).redirect('/login');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ jest.mock('~/server/services/GraphTokenService', () => ({
|
|||
jest.mock('~/server/services/AuthService', () => ({
|
||||
requestPasswordReset: jest.fn(),
|
||||
setOpenIDAuthTokens: jest.fn(),
|
||||
setCloudFrontAuthCookies: jest.fn(),
|
||||
resetPassword: jest.fn(),
|
||||
setAuthTokens: jest.fn(),
|
||||
registerUser: jest.fn(),
|
||||
|
|
@ -37,16 +38,18 @@ jest.mock('@librechat/api', () => ({
|
|||
}));
|
||||
|
||||
const openIdClient = require('openid-client');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { isEnabled, findOpenIDUser, buildOpenIDRefreshParams } = require('@librechat/api');
|
||||
const { graphTokenController, refreshController } = require('./AuthController');
|
||||
const { getGraphApiToken } = require('~/server/services/GraphTokenService');
|
||||
const { setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
||||
const { setOpenIDAuthTokens, setCloudFrontAuthCookies } = require('~/server/services/AuthService');
|
||||
const { getOpenIdConfig, getOpenIdEmail } = require('~/strategies');
|
||||
const { updateUser } = require('~/models');
|
||||
const { getUserById, updateUser } = require('~/models');
|
||||
|
||||
const ORIGINAL_OPENID_SCOPE = process.env.OPENID_SCOPE;
|
||||
const ORIGINAL_OPENID_REFRESH_AUDIENCE = process.env.OPENID_REFRESH_AUDIENCE;
|
||||
const ORIGINAL_JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
|
||||
|
||||
describe('graphTokenController', () => {
|
||||
let req, res;
|
||||
|
|
@ -196,6 +199,7 @@ describe('refreshController – OpenID path', () => {
|
|||
jest.clearAllMocks();
|
||||
delete process.env.OPENID_SCOPE;
|
||||
delete process.env.OPENID_REFRESH_AUDIENCE;
|
||||
process.env.JWT_REFRESH_SECRET = 'test-refresh-secret';
|
||||
|
||||
isEnabled.mockReturnValue(true);
|
||||
getOpenIdConfig.mockReturnValue({ some: 'config' });
|
||||
|
|
@ -203,7 +207,13 @@ describe('refreshController – OpenID path', () => {
|
|||
mockTokenset.claims.mockReturnValue(baseClaims);
|
||||
getOpenIdEmail.mockReturnValue(baseClaims.email);
|
||||
setOpenIDAuthTokens.mockReturnValue('new-app-token');
|
||||
setCloudFrontAuthCookies.mockReturnValue(true);
|
||||
findOpenIDUser.mockResolvedValue({ user: { ...defaultUser }, error: null, migration: false });
|
||||
getUserById.mockResolvedValue({
|
||||
_id: 'user-db-id',
|
||||
email: baseClaims.email,
|
||||
openidId: baseClaims.sub,
|
||||
});
|
||||
updateUser.mockResolvedValue({});
|
||||
|
||||
req = {
|
||||
|
|
@ -230,6 +240,12 @@ describe('refreshController – OpenID path', () => {
|
|||
} else {
|
||||
process.env.OPENID_REFRESH_AUDIENCE = ORIGINAL_OPENID_REFRESH_AUDIENCE;
|
||||
}
|
||||
|
||||
if (ORIGINAL_JWT_REFRESH_SECRET === undefined) {
|
||||
delete process.env.JWT_REFRESH_SECRET;
|
||||
} else {
|
||||
process.env.JWT_REFRESH_SECRET = ORIGINAL_JWT_REFRESH_SECRET;
|
||||
}
|
||||
});
|
||||
|
||||
it('should call getOpenIdEmail with token claims and use result for findOpenIDUser', async () => {
|
||||
|
|
@ -246,6 +262,70 @@ describe('refreshController – OpenID path', () => {
|
|||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it('reuses valid OpenID session tokens and refreshes CloudFront cookies', async () => {
|
||||
const reusableIdToken = jwt.sign(
|
||||
{ sub: baseClaims.sub, exp: Math.floor(Date.now() / 1000) + 3600 },
|
||||
'idp-signing-secret',
|
||||
);
|
||||
const signedUserId = jwt.sign({ id: 'user-db-id' }, process.env.JWT_REFRESH_SECRET, {
|
||||
expiresIn: '1h',
|
||||
});
|
||||
req.headers.cookie = [
|
||||
'token_provider=openid',
|
||||
'refreshToken=stored-refresh',
|
||||
`openid_user_id=${signedUserId}`,
|
||||
].join('; ');
|
||||
req.session = {
|
||||
openidTokens: {
|
||||
accessToken: 'session-access-token',
|
||||
idToken: reusableIdToken,
|
||||
refreshToken: 'stored-refresh',
|
||||
},
|
||||
};
|
||||
const user = {
|
||||
...defaultUser,
|
||||
federatedTokens: { access_token: 'do-not-return' },
|
||||
};
|
||||
getUserById.mockResolvedValue(user);
|
||||
|
||||
await refreshController(req, res);
|
||||
|
||||
expect(openIdClient.refreshTokenGrant).not.toHaveBeenCalled();
|
||||
expect(setOpenIDAuthTokens).not.toHaveBeenCalled();
|
||||
expect(getUserById).toHaveBeenCalledWith(
|
||||
'user-db-id',
|
||||
'-password -__v -totpSecret -backupCodes -federatedTokens',
|
||||
);
|
||||
expect(setCloudFrontAuthCookies).toHaveBeenCalledWith(req, res, user);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
token: reusableIdToken,
|
||||
user: expect.objectContaining({
|
||||
_id: 'user-db-id',
|
||||
email: baseClaims.email,
|
||||
openidId: baseClaims.sub,
|
||||
}),
|
||||
});
|
||||
|
||||
const sentPayload = res.send.mock.calls[0][0];
|
||||
expect(sentPayload.user).not.toHaveProperty('password');
|
||||
expect(sentPayload.user).not.toHaveProperty('totpSecret');
|
||||
expect(sentPayload.user).not.toHaveProperty('backupCodes');
|
||||
expect(sentPayload.user).not.toHaveProperty('federatedTokens');
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'[refreshController] OpenID session token reused',
|
||||
expect.objectContaining({
|
||||
token_type: 'id_token',
|
||||
cloudfront_cookies_attempted: true,
|
||||
cloudfront_cookies_set: true,
|
||||
}),
|
||||
);
|
||||
const debugOutput = JSON.stringify(logger.debug.mock.calls);
|
||||
expect(debugOutput).not.toContain(reusableIdToken);
|
||||
expect(debugOutput).not.toContain(signedUserId);
|
||||
expect(debugOutput).not.toContain('session-access-token');
|
||||
});
|
||||
|
||||
it('should pass scope-only OpenID refresh params when OPENID_SCOPE is set', async () => {
|
||||
process.env.OPENID_SCOPE = 'openid profile email';
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const {
|
|||
isEnabled,
|
||||
checkEmailConfig,
|
||||
setCloudFrontCookies,
|
||||
getCloudFrontConfig,
|
||||
parseCloudFrontCookieScope,
|
||||
CLOUDFRONT_SCOPE_COOKIE,
|
||||
isEmailDomainAllowed,
|
||||
|
|
@ -411,6 +412,85 @@ const resetPassword = async (userId, token, password) => {
|
|||
const getPreviousCloudFrontScope = (req) =>
|
||||
parseCloudFrontCookieScope(req?.cookies?.[CLOUDFRONT_SCOPE_COOKIE]);
|
||||
|
||||
const normalizeCloudFrontScopeValue = (value) => {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
return value.toString?.() ?? value;
|
||||
};
|
||||
|
||||
const getCloudFrontScopeValue = (optionsValue, userValue, requestValue) =>
|
||||
normalizeCloudFrontScopeValue(optionsValue ?? userValue ?? requestValue);
|
||||
|
||||
const getCloudFrontAuthCookieSkipReason = (scope) => {
|
||||
const config = getCloudFrontConfig();
|
||||
if (!config || config.imageSigning !== 'cookies' || !config.privateKey || !config.keyPairId) {
|
||||
return 'cloudfront_disabled';
|
||||
}
|
||||
if (!config.cookieDomain) {
|
||||
return 'missing_cookie_domain';
|
||||
}
|
||||
if (!scope.userId) {
|
||||
return 'missing_user_id';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes CloudFront signed cookies for authenticated image/avatar access.
|
||||
* @param {ServerRequest | null} req
|
||||
* @param {ServerResponse} res
|
||||
* @param {Partial<IUser> | null} user
|
||||
* @param {import('@librechat/api').CloudFrontCookieScope & { orgId?: string }} [options={}]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const setCloudFrontAuthCookies = (req, res, user, options = {}) => {
|
||||
const storageRegion = getCloudFrontScopeValue(
|
||||
options.storageRegion,
|
||||
user?.storageRegion,
|
||||
req?.user?.storageRegion,
|
||||
);
|
||||
const scope = {
|
||||
userId: getCloudFrontScopeValue(
|
||||
options.userId,
|
||||
user?._id ?? user?.id,
|
||||
req?.user?._id ?? req?.user?.id,
|
||||
),
|
||||
tenantId: getCloudFrontScopeValue(
|
||||
options.tenantId ?? options.orgId,
|
||||
user?.tenantId ?? user?.orgId,
|
||||
req?.user?.tenantId ?? req?.user?.orgId,
|
||||
),
|
||||
...(storageRegion ? { storageRegion } : {}),
|
||||
};
|
||||
const skipReason = getCloudFrontAuthCookieSkipReason(scope);
|
||||
if (skipReason) {
|
||||
logger.debug('[setCloudFrontAuthCookies] CloudFront auth cookies skipped', {
|
||||
attempted: false,
|
||||
set: false,
|
||||
reason: skipReason,
|
||||
has_user_id: Boolean(scope.userId),
|
||||
has_tenant_scope: Boolean(scope.tenantId),
|
||||
has_storage_region: Boolean(scope.storageRegion),
|
||||
has_previous_scope: Boolean(getPreviousCloudFrontScope(req)?.userId),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousScope = getPreviousCloudFrontScope(req);
|
||||
const cookiesSet = setCloudFrontCookies(res, scope, previousScope);
|
||||
logger.debug('[setCloudFrontAuthCookies] CloudFront auth cookies refreshed', {
|
||||
attempted: true,
|
||||
set: cookiesSet,
|
||||
reason: cookiesSet ? undefined : 'set_failed',
|
||||
has_user_id: true,
|
||||
has_tenant_scope: Boolean(scope.tenantId),
|
||||
has_storage_region: Boolean(scope.storageRegion),
|
||||
has_previous_scope: Boolean(previousScope?.userId),
|
||||
});
|
||||
return cookiesSet;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set Auth Tokens
|
||||
* @param {String | ObjectId} userId
|
||||
|
|
@ -453,14 +533,7 @@ const setAuthTokens = async (userId, res, _session = null, req = null) => {
|
|||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
setCloudFrontCookies(
|
||||
res,
|
||||
{
|
||||
userId: user?._id?.toString?.() ?? userId,
|
||||
tenantId: user?.tenantId?.toString?.(),
|
||||
},
|
||||
getPreviousCloudFrontScope(req),
|
||||
);
|
||||
setCloudFrontAuthCookies(req, res, user, { userId });
|
||||
|
||||
return token;
|
||||
} catch (error) {
|
||||
|
|
@ -609,14 +682,7 @@ const setOpenIDAuthTokens = (
|
|||
});
|
||||
}
|
||||
|
||||
setCloudFrontCookies(
|
||||
res,
|
||||
{
|
||||
userId,
|
||||
tenantId: tenantId ?? req.user?.tenantId,
|
||||
},
|
||||
getPreviousCloudFrontScope(req),
|
||||
);
|
||||
setCloudFrontAuthCookies(req, res, req.user, { userId, tenantId });
|
||||
|
||||
return appAuthToken;
|
||||
} catch (error) {
|
||||
|
|
@ -691,6 +757,7 @@ module.exports = {
|
|||
setAuthTokens,
|
||||
resetPassword,
|
||||
setOpenIDAuthTokens,
|
||||
setCloudFrontAuthCookies,
|
||||
requestPasswordReset,
|
||||
resendVerificationEmail,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ jest.mock('@librechat/api', () => ({
|
|||
shouldUseSecureCookie: jest.fn(() => false),
|
||||
resolveAppConfigForUser: jest.fn(async (_getAppConfig, _user) => ({})),
|
||||
setCloudFrontCookies: jest.fn(() => true),
|
||||
getCloudFrontConfig: jest.fn(() => ({
|
||||
domain: 'https://cdn.example.com',
|
||||
imageSigning: 'cookies',
|
||||
cookieDomain: '.example.com',
|
||||
privateKey: 'test-private-key',
|
||||
keyPairId: 'K123ABC',
|
||||
})),
|
||||
parseCloudFrontCookieScope: jest.fn(() => null),
|
||||
CLOUDFRONT_SCOPE_COOKIE: 'LibreChat-CloudFront-Scope',
|
||||
}));
|
||||
|
|
@ -44,8 +51,10 @@ const {
|
|||
isEmailDomainAllowed,
|
||||
resolveAppConfigForUser,
|
||||
setCloudFrontCookies,
|
||||
getCloudFrontConfig,
|
||||
parseCloudFrontCookieScope,
|
||||
} = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
findUser,
|
||||
getUserById,
|
||||
|
|
@ -54,7 +63,12 @@ const {
|
|||
createSession,
|
||||
} = require('~/models');
|
||||
const { getAppConfig } = require('~/server/services/Config');
|
||||
const { setOpenIDAuthTokens, requestPasswordReset, setAuthTokens } = require('./AuthService');
|
||||
const {
|
||||
setOpenIDAuthTokens,
|
||||
requestPasswordReset,
|
||||
setAuthTokens,
|
||||
setCloudFrontAuthCookies,
|
||||
} = require('./AuthService');
|
||||
|
||||
/** Helper to build a mock Express response */
|
||||
function mockResponse() {
|
||||
|
|
@ -376,8 +390,195 @@ describe('requestPasswordReset', () => {
|
|||
});
|
||||
|
||||
describe('CloudFront cookie integration', () => {
|
||||
const cloudFrontCookieConfig = {
|
||||
domain: 'https://cdn.example.com',
|
||||
imageSigning: 'cookies',
|
||||
cookieDomain: '.example.com',
|
||||
privateKey: 'test-private-key',
|
||||
keyPairId: 'K123ABC',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
getCloudFrontConfig.mockReturnValue(cloudFrontCookieConfig);
|
||||
setCloudFrontCookies.mockReturnValue(true);
|
||||
parseCloudFrontCookieScope.mockReturnValue(null);
|
||||
});
|
||||
|
||||
describe('setCloudFrontAuthCookies', () => {
|
||||
it('passes user id and tenant scope from the user', () => {
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
const user = {
|
||||
_id: { toString: () => 'user-123' },
|
||||
tenantId: { toString: () => 'tenantA' },
|
||||
};
|
||||
|
||||
const result = setCloudFrontAuthCookies(req, res, user);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(setCloudFrontCookies).toHaveBeenCalledWith(
|
||||
res,
|
||||
{
|
||||
userId: 'user-123',
|
||||
tenantId: 'tenantA',
|
||||
},
|
||||
null,
|
||||
);
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'[setCloudFrontAuthCookies] CloudFront auth cookies refreshed',
|
||||
expect.objectContaining({
|
||||
attempted: true,
|
||||
set: true,
|
||||
has_user_id: true,
|
||||
has_tenant_scope: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('lets explicit scope options override user and request scope', () => {
|
||||
const req = mockRequest();
|
||||
req.user = { _id: 'request-user', tenantId: 'request-tenant' };
|
||||
const res = mockResponse();
|
||||
const user = { _id: 'user-123', tenantId: 'tenantA' };
|
||||
|
||||
setCloudFrontAuthCookies(req, res, user, {
|
||||
userId: 'option-user',
|
||||
tenantId: 'option-tenant',
|
||||
storageRegion: 'us-east-2',
|
||||
});
|
||||
|
||||
expect(setCloudFrontCookies).toHaveBeenCalledWith(
|
||||
res,
|
||||
{
|
||||
userId: 'option-user',
|
||||
tenantId: 'option-tenant',
|
||||
storageRegion: 'us-east-2',
|
||||
},
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to request tenant scope when the user has none', () => {
|
||||
const req = mockRequest();
|
||||
req.user = { tenantId: 'request-tenant' };
|
||||
const res = mockResponse();
|
||||
|
||||
setCloudFrontAuthCookies(req, res, { _id: 'user-123' });
|
||||
|
||||
expect(setCloudFrontCookies).toHaveBeenCalledWith(
|
||||
res,
|
||||
{
|
||||
userId: 'user-123',
|
||||
tenantId: 'request-tenant',
|
||||
},
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses org scope as tenant scope when tenantId is unavailable', () => {
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
setCloudFrontAuthCookies(req, res, { _id: 'user-123', orgId: 'orgA' });
|
||||
|
||||
expect(setCloudFrontCookies).toHaveBeenCalledWith(
|
||||
res,
|
||||
{
|
||||
userId: 'user-123',
|
||||
tenantId: 'orgA',
|
||||
},
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses previous CloudFront scope for stale cookie cleanup', () => {
|
||||
parseCloudFrontCookieScope.mockReturnValue({ userId: 'old-user', tenantId: 'old-tenant' });
|
||||
const req = mockRequest({}, { 'LibreChat-CloudFront-Scope': 'encoded-scope' });
|
||||
const res = mockResponse();
|
||||
|
||||
setCloudFrontAuthCookies(req, res, { _id: 'user-123', tenantId: 'tenantA' });
|
||||
|
||||
expect(parseCloudFrontCookieScope).toHaveBeenCalledWith('encoded-scope');
|
||||
expect(setCloudFrontCookies).toHaveBeenCalledWith(
|
||||
res,
|
||||
{
|
||||
userId: 'user-123',
|
||||
tenantId: 'tenantA',
|
||||
},
|
||||
{ userId: 'old-user', tenantId: 'old-tenant' },
|
||||
);
|
||||
});
|
||||
|
||||
it('no-ops when CloudFront cookie signing is disabled', () => {
|
||||
getCloudFrontConfig.mockReturnValue({ ...cloudFrontCookieConfig, imageSigning: 'none' });
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
const result = setCloudFrontAuthCookies(req, res, { _id: 'user-123' });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(setCloudFrontCookies).not.toHaveBeenCalled();
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'[setCloudFrontAuthCookies] CloudFront auth cookies skipped',
|
||||
expect.objectContaining({
|
||||
attempted: false,
|
||||
set: false,
|
||||
reason: 'cloudfront_disabled',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('fails closed when user id is missing', () => {
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
const result = setCloudFrontAuthCookies(req, res, { tenantId: 'tenantA' });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(setCloudFrontCookies).not.toHaveBeenCalled();
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'[setCloudFrontAuthCookies] CloudFront auth cookies skipped',
|
||||
expect.objectContaining({
|
||||
attempted: false,
|
||||
set: false,
|
||||
reason: 'missing_user_id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('skips when CloudFront cookie domain is missing', () => {
|
||||
getCloudFrontConfig.mockReturnValue({ ...cloudFrontCookieConfig, cookieDomain: null });
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
const result = setCloudFrontAuthCookies(req, res, { _id: 'user-123' });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(setCloudFrontCookies).not.toHaveBeenCalled();
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'[setCloudFrontAuthCookies] CloudFront auth cookies skipped',
|
||||
expect.objectContaining({
|
||||
attempted: false,
|
||||
set: false,
|
||||
reason: 'missing_cookie_domain',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not log cookie secrets or signed-cookie values', () => {
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
setCloudFrontAuthCookies(req, res, { _id: 'user-123' });
|
||||
|
||||
const debugOutput = JSON.stringify(logger.debug.mock.calls);
|
||||
expect(debugOutput).not.toContain('test-private-key');
|
||||
expect(debugOutput).not.toContain('K123ABC');
|
||||
expect(debugOutput).not.toContain('CloudFront-Policy');
|
||||
expect(debugOutput).not.toContain('CloudFront-Signature');
|
||||
expect(debugOutput).not.toContain('CloudFront-Key-Pair-Id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOpenIDAuthTokens', () => {
|
||||
|
|
@ -429,13 +630,14 @@ describe('CloudFront cookie integration', () => {
|
|||
const result = setOpenIDAuthTokens(validTokenset, req, res, null);
|
||||
|
||||
expect(result).toBe('the-id-token');
|
||||
expect(setCloudFrontCookies).toHaveBeenCalledWith(
|
||||
res,
|
||||
{
|
||||
userId: null,
|
||||
tenantId: undefined,
|
||||
},
|
||||
null,
|
||||
expect(setCloudFrontCookies).not.toHaveBeenCalled();
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'[setCloudFrontAuthCookies] CloudFront auth cookies skipped',
|
||||
expect.objectContaining({
|
||||
attempted: false,
|
||||
set: false,
|
||||
reason: 'missing_user_id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -446,13 +648,14 @@ describe('CloudFront cookie integration', () => {
|
|||
const result = setOpenIDAuthTokens(validTokenset, req, res);
|
||||
|
||||
expect(result).toBe('the-id-token');
|
||||
expect(setCloudFrontCookies).toHaveBeenCalledWith(
|
||||
res,
|
||||
{
|
||||
userId: null,
|
||||
tenantId: undefined,
|
||||
},
|
||||
null,
|
||||
expect(setCloudFrontCookies).not.toHaveBeenCalled();
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'[setCloudFrontAuthCookies] CloudFront auth cookies skipped',
|
||||
expect.objectContaining({
|
||||
attempted: false,
|
||||
set: false,
|
||||
reason: 'missing_user_id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -463,13 +666,14 @@ describe('CloudFront cookie integration', () => {
|
|||
const result = setOpenIDAuthTokens(validTokenset, req, res, {});
|
||||
|
||||
expect(result).toBe('the-id-token');
|
||||
expect(setCloudFrontCookies).toHaveBeenCalledWith(
|
||||
res,
|
||||
{
|
||||
userId: undefined,
|
||||
tenantId: undefined,
|
||||
},
|
||||
null,
|
||||
expect(setCloudFrontCookies).not.toHaveBeenCalled();
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'[setCloudFrontAuthCookies] CloudFront auth cookies skipped',
|
||||
expect.objectContaining({
|
||||
attempted: false,
|
||||
set: false,
|
||||
reason: 'missing_user_id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue