From 17a08224e1872aee1e12c23c328f710e2ac7dc5a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 11 May 2026 22:33:27 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8D=AA=20fix:=20Refresh=20CloudFront=20Co?= =?UTF-8?q?okies=20On=20Auth=20Refresh=20(#13083)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Refresh CloudFront Cookies On Auth Refresh * fix: Exclude Federated Tokens From Refresh Lookup --- api/server/controllers/AuthController.js | 77 +++++- api/server/controllers/AuthController.spec.js | 84 +++++- api/server/services/AuthService.js | 99 +++++-- api/server/services/AuthService.spec.js | 248 ++++++++++++++++-- 4 files changed, 465 insertions(+), 43 deletions(-) diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index 16532851b5..3f24e56abe 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -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'); } diff --git a/api/server/controllers/AuthController.spec.js b/api/server/controllers/AuthController.spec.js index b8d0aa3570..cbfd483e33 100644 --- a/api/server/controllers/AuthController.spec.js +++ b/api/server/controllers/AuthController.spec.js @@ -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'; diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 659e915f44..7197002873 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -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 | 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, }; diff --git a/api/server/services/AuthService.spec.js b/api/server/services/AuthService.spec.js index 8209d46d25..ab2be3a57d 100644 --- a/api/server/services/AuthService.spec.js +++ b/api/server/services/AuthService.spec.js @@ -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', + }), ); });