Skip to content
Draft
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
20 changes: 16 additions & 4 deletions api/cache/banViolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { logger } = require('@librechat/data-schemas');
const { ViolationTypes } = require('librechat-data-provider');
const { isEnabled, math, removePorts } = require('@librechat/api');
const { deleteAllUserSessions } = require('~/models');
const { getTokenCustodyService } = require('~/server/services/AuthService');
const getLogStores = require('./getLogStores');

const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};
Expand Down Expand Up @@ -47,16 +48,27 @@ 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;
}
/**
* Delete every custody record for the banned user, the custody counterpart to
* deleteAllUserSessions. The server never holds the token key, so deleting the record is the
* only way to revoke the sealed tokens from here.
*/
await getTokenCustodyService().deleteAllForUser({
userId: user_id,
tenantId: req.user?.tenantId,
});

/**
* `req.session.openidTokens` is retired by key custody; the deleteAllForUser call above removes
* the sealed custody records, which is what revokes the OpenID session now. The cookie clears
* stay as defensive cleanup for any cookies a pre-rollout session may still carry.
*/
res.clearCookie('refreshToken');
res.clearCookie('openid_access_token');
res.clearCookie('openid_id_token');
res.clearCookie('openid_user_id');
res.clearCookie('token_provider');
res.clearCookie('openid_token_key');

const banLogs = getLogStores(ViolationTypes.BAN);
const duration = errorMessage.duration || banLogs.opts.ttl;
Expand Down
24 changes: 24 additions & 0 deletions api/cache/banViolation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ jest.mock('~/models', () => ({
deleteAllUserSessions: jest.fn().mockResolvedValue(true),
}));

// The ban sweep deletes every custody record for the user alongside the session sweep.
const mockDeleteAllForUser = jest.fn().mockResolvedValue(undefined);
jest.mock('~/server/services/AuthService', () => ({
getTokenCustodyService: () => ({ deleteAllForUser: mockDeleteAllForUser }),
}));

describe('banViolation', () => {
let mongoServer;
let req, res, errorMessage;
Expand All @@ -26,6 +32,7 @@ describe('banViolation', () => {
beforeEach(() => {
req = {
ip: '127.0.0.1',
user: { id: 'user-1', tenantId: 'tenant-a' },
cookies: {
refreshToken: 'someToken',
},
Expand Down Expand Up @@ -105,6 +112,23 @@ describe('banViolation', () => {
expect(res.clearCookie).toHaveBeenCalledWith('refreshToken');
});

it('sweeps the user custody records alongside the sessions when the ban threshold is crossed', async () => {
errorMessage.prev_count = 19;
errorMessage.violation_count = 39;
await banViolation(req, res, errorMessage);
expect(mockDeleteAllForUser).toHaveBeenCalledWith({
userId: errorMessage.user_id,
tenantId: 'tenant-a',
});
});

it('does not sweep custody records when the threshold is not crossed', async () => {
errorMessage.prev_count = 0;
errorMessage.violation_count = 19;
await banViolation(req, res, errorMessage);
expect(mockDeleteAllForUser).not.toHaveBeenCalled();
});

it('should not ban if violation_count does not change', async () => {
errorMessage.prev_count = 0;
errorMessage.violation_count = 0;
Expand Down
Loading
Loading