Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/server/controllers/auth/LogoutController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const cookies = require('cookie');
const { isEnabled } = require('@librechat/api');
const { isEnabled, clearCloudFrontCookies } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { logoutUser } = require('~/server/services/AuthService');
const { getOpenIdConfig } = require('~/strategies');
Expand Down Expand Up @@ -28,6 +28,7 @@ const logoutController = async (req, res) => {
res.clearCookie('openid_id_token');
res.clearCookie('openid_user_id');
res.clearCookie('token_provider');
clearCloudFrontCookies(res);
const response = { message };
if (
isOpenIdUser &&
Expand Down
15 changes: 14 additions & 1 deletion api/server/controllers/auth/LogoutController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ const mockLogoutUser = jest.fn();
const mockLogger = { warn: jest.fn(), error: jest.fn() };
const mockIsEnabled = jest.fn();
const mockGetOpenIdConfig = jest.fn();
const mockClearCloudFrontCookies = jest.fn();

jest.mock('cookie');
jest.mock('@librechat/api', () => ({ isEnabled: (...args) => mockIsEnabled(...args) }));
jest.mock('@librechat/api', () => ({
isEnabled: (...args) => mockIsEnabled(...args),
clearCloudFrontCookies: (...args) => mockClearCloudFrontCookies(...args),
}));
jest.mock('@librechat/data-schemas', () => ({ logger: mockLogger }));
jest.mock('~/server/services/AuthService', () => ({
logoutUser: (...args) => mockLogoutUser(...args),
Expand Down Expand Up @@ -255,5 +259,14 @@ describe('LogoutController', () => {
expect(res.clearCookie).toHaveBeenCalledWith('openid_user_id');
expect(res.clearCookie).toHaveBeenCalledWith('token_provider');
});

it('calls clearCloudFrontCookies on successful logout', async () => {
const req = buildReq();
const res = buildRes();

await logoutController(req, res);

expect(mockClearCloudFrontCookies).toHaveBeenCalledWith(res);
});
});
});
7 changes: 7 additions & 0 deletions api/server/services/AuthService.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
math,
isEnabled,
checkEmailConfig,
setCloudFrontCookies,
isEmailDomainAllowed,
shouldUseSecureCookie,
} = require('@librechat/api');
Expand Down Expand Up @@ -406,6 +407,9 @@ const setAuthTokens = async (userId, res, _session = null) => {
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});

setCloudFrontCookies(res);

return token;
} catch (error) {
logger.error('[setAuthTokens] Error in setting authentication tokens:', error);
Expand Down Expand Up @@ -523,6 +527,9 @@ const setOpenIDAuthTokens = (tokenset, req, res, userId, existingRefreshToken) =
sameSite: 'strict',
});
}

setCloudFrontCookies(res);

return appAuthToken;
} catch (error) {
logger.error('[setOpenIDAuthTokens] Error in setting authentication tokens:', error);
Expand Down
70 changes: 68 additions & 2 deletions api/server/services/AuthService.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jest.mock('@librechat/api', () => ({
isEmailDomainAllowed: jest.fn(),
math: jest.fn((val, fallback) => (val ? Number(val) : fallback)),
shouldUseSecureCookie: jest.fn(() => false),
setCloudFrontCookies: jest.fn(() => true),
}));
jest.mock('~/models', () => ({
findUser: jest.fn(),
Expand All @@ -35,8 +36,9 @@ jest.mock('~/strategies/validators', () => ({ registerSchema: { parse: jest.fn()
jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn() }));
jest.mock('~/server/utils', () => ({ sendEmail: jest.fn() }));

const { shouldUseSecureCookie } = require('@librechat/api');
const { setOpenIDAuthTokens } = require('./AuthService');
const { shouldUseSecureCookie, setCloudFrontCookies } = require('@librechat/api');
const { createSession, generateToken, generateRefreshToken, getUserById } = require('~/models');
const { setOpenIDAuthTokens, setAuthTokens } = require('./AuthService');

/** Helper to build a mock Express response */
function mockResponse() {
Expand Down Expand Up @@ -267,3 +269,67 @@ describe('setOpenIDAuthTokens', () => {
});
});
});

describe('CloudFront cookie integration', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('setOpenIDAuthTokens', () => {
const validTokenset = {
id_token: 'the-id-token',
access_token: 'the-access-token',
refresh_token: 'the-refresh-token',
};

it('calls setCloudFrontCookies with response object', () => {
const req = mockRequest();
const res = mockResponse();

setOpenIDAuthTokens(validTokenset, req, res, 'user-123');

expect(setCloudFrontCookies).toHaveBeenCalledWith(res);
});

it('succeeds even when setCloudFrontCookies returns false', () => {
setCloudFrontCookies.mockReturnValue(false);

const req = mockRequest();
const res = mockResponse();

const result = setOpenIDAuthTokens(validTokenset, req, res, 'user-123');

expect(result).toBe('the-id-token');
});
});

describe('setAuthTokens', () => {
beforeEach(() => {
getUserById.mockResolvedValue({ _id: 'user-123' });
generateToken.mockResolvedValue('mock-access-token');
generateRefreshToken.mockReturnValue('mock-refresh-token');
createSession.mockResolvedValue({
session: { expiration: new Date(Date.now() + 604800000) },
refreshToken: 'mock-refresh-token',
});
});

it('calls setCloudFrontCookies with response object', async () => {
const res = mockResponse();

await setAuthTokens('user-123', res);

expect(setCloudFrontCookies).toHaveBeenCalledWith(res);
});

it('succeeds even when setCloudFrontCookies returns false', async () => {
setCloudFrontCookies.mockReturnValue(false);

const res = mockResponse();

const result = await setAuthTokens('user-123', res);

expect(result).toBe('mock-access-token');
});
});
});
7 changes: 6 additions & 1 deletion api/server/services/Files/images/encode.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ const base64Only = new Set([
EModelEndpoint.bedrock,
]);

const blobStorageSources = new Set([FileSources.azure_blob, FileSources.s3, FileSources.firebase]);
const blobStorageSources = new Set([
FileSources.azure_blob,
FileSources.s3,
FileSources.firebase,
FileSources.cloudfront,
]);

/**
* Encodes and formats the given files.
Expand Down
52 changes: 27 additions & 25 deletions librechat.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,17 @@ cache: true
# CloudFront CDN Configuration (optional)
# Use when fileStrategy: "cloudfront" or fileStrategies includes cloudfront
# Requires: AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_BUCKET_NAME
# For signed URLs: CLOUDFRONT_KEY_PAIR_ID, CLOUDFRONT_PRIVATE_KEY
# For signed cookies/URLs: CLOUDFRONT_KEY_PAIR_ID, CLOUDFRONT_PRIVATE_KEY
# cloudfront:
# domain: "https://d1234abcd.cloudfront.net"
# distributionId: "E1234ABCD" # Required if invalidateOnDelete is true
# invalidateOnDelete: false # Create cache invalidation on file delete
# # NOTE: imageSigning is not yet implemented. All URLs are unsigned regardless of value.
# # Do NOT restrict your CloudFront distribution to signed requests.
# imageSigning: "none" # "none" | "cookies" | "url"
# urlExpiry: 3600 # Signed URL expiry in seconds
# domain: "https://cdn.example.com" # CloudFront domain (CNAME recommended for cookies)
# distributionId: "E1234ABCD" # Required if invalidateOnDelete is true
# invalidateOnDelete: false # Create cache invalidation on file delete
# imageSigning: "none" # "none" (public) | "cookies" (signed cookies)
# # When imageSigning: "cookies", API + CloudFront must share a parent domain:
# # API: api.example.com, CloudFront CNAME: cdn.example.com, cookieDomain: ".example.com"
# cookieDomain: ".example.com" # Required for "cookies" - shared parent domain
# cookieExpiry: 1800 # Cookie lifetime in seconds (max: 604800 / 7 days, default: 1800 / 30 min)
# urlExpiry: 3600 # Reserved for future signed-URL mode (not yet implemented)

# Custom interface configuration
interface:
Expand Down Expand Up @@ -127,23 +129,23 @@ interface:
# public: false
# MCP Servers configuration example
# mcpServers:
# Controls user permissions for MCP (Model Context Protocol) server management
# - use: Allow users to use configured MCP servers
# - create: Allow users to create and manage new MCP servers
# - share: Allow users to share MCP servers with other users
# - public: Allow users to share MCP servers publicly (with everyone)
# Creation / edit MCP server config Dialog config example
# trustCheckbox:
# label:
# en: 'I understand and I want to continue'
# de: 'Ich verstehe und möchte fortfahren'
# de-DE: 'Ich verstehe und möchte fortfahren' # You can narrow translation to regions like (de-DE or de-CH)
# subLabel:
# en: |
# Librechat hasn't reviewed this MCP server. Attackers may attempt to steal your data or trick the model into taking unintended actions, including destroying data. <a href="https://google.de" target="_blank"><strong>Learn more.</strong></a>
# de: |
# LibreChat hat diesen MCP-Server nicht überprüft. Angreifer könnten versuchen, Ihre Daten zu stehlen oder das Modell zu unbeabsichtigten Aktionen zu verleiten, einschließlich der Zerstörung von Daten. <a href="https://google.de" target="_blank"><strong>Mehr erfahren.</strong></a>
# Controls user permissions for MCP (Model Context Protocol) server management
# - use: Allow users to use configured MCP servers
# - create: Allow users to create and manage new MCP servers
# - share: Allow users to share MCP servers with other users
# - public: Allow users to share MCP servers publicly (with everyone)

# Creation / edit MCP server config Dialog config example
# trustCheckbox:
# label:
# en: 'I understand and I want to continue'
# de: 'Ich verstehe und möchte fortfahren'
# de-DE: 'Ich verstehe und möchte fortfahren' # You can narrow translation to regions like (de-DE or de-CH)
# subLabel:
# en: |
# Librechat hasn't reviewed this MCP server. Attackers may attempt to steal your data or trick the model into taking unintended actions, including destroying data. <a href="https://google.de" target="_blank"><strong>Learn more.</strong></a>
# de: |
# LibreChat hat diesen MCP-Server nicht überprüft. Angreifer könnten versuchen, Ihre Daten zu stehlen oder das Modell zu unbeabsichtigten Aktionen zu verleiten, einschließlich der Zerstörung von Daten. <a href="https://google.de" target="_blank"><strong>Mehr erfahren.</strong></a>

# Temporary chat retention period in hours (default: 720, min: 1, max: 8760)
# temporaryChatRetention: 1
Expand Down
Loading
Loading