mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-10 08:31:37 +00:00
🛠️ fix: type-safety polish on OBO data layer
Replace refresh token bridge query/update Record<string, unknown> usage with typed Mongoose FilterQuery and UpdateQuery definitions. Harden OpenID marker cookie JWT expiry handling by converting refresh expiry milliseconds to integer seconds and rejecting invalid or non-positive durations. Add focused CSRF tests for fractional refresh expiry values and invalid expiry configuration.
This commit is contained in:
parent
5a32d9eb3d
commit
cc74590864
3 changed files with 47 additions and 13 deletions
|
|
@ -241,4 +241,40 @@ describe('setOpenIDMarkerCookies', () => {
|
|||
expect.objectContaining({ expires }),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses integer seconds for fractional refresh expiry durations', () => {
|
||||
const res = { cookie: jest.fn() } as unknown as import('express').Response;
|
||||
const expires = new Date(Date.now() + 604800999);
|
||||
|
||||
setOpenIDMarkerCookies(res, {
|
||||
userId: 'user-123',
|
||||
expires,
|
||||
refreshExpiryMs: 604800999,
|
||||
});
|
||||
|
||||
const signedUserId = (res.cookie as jest.Mock).mock.calls.find(
|
||||
([name]) => name === OPENID_USER_ID_COOKIE,
|
||||
)?.[1];
|
||||
const payload = jwt.verify(signedUserId, 'marker-secret') as jwt.JwtPayload;
|
||||
if (typeof payload.exp !== 'number' || typeof payload.iat !== 'number') {
|
||||
throw new Error('Expected signed marker JWT to include numeric exp and iat');
|
||||
}
|
||||
expect(payload.exp - payload.iat).toBe(604800);
|
||||
});
|
||||
|
||||
it.each([0, -1000, 999, Number.NaN, Number.POSITIVE_INFINITY])(
|
||||
'throws when the refresh expiry duration is invalid: %p',
|
||||
(refreshExpiryMs) => {
|
||||
const res = { cookie: jest.fn() } as unknown as import('express').Response;
|
||||
const expires = new Date(Date.now() + 999);
|
||||
|
||||
expect(() =>
|
||||
setOpenIDMarkerCookies(res, {
|
||||
userId: 'user-123',
|
||||
expires,
|
||||
refreshExpiryMs,
|
||||
}),
|
||||
).toThrow('refreshExpiryMs must be a positive duration for OpenID marker cookies');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -104,8 +104,13 @@ export function setOpenIDMarkerCookies(
|
|||
throw new Error('JWT_REFRESH_SECRET is required for OpenID marker cookies');
|
||||
}
|
||||
|
||||
const refreshExpirySeconds = Math.floor(refreshExpiryMs / 1000);
|
||||
if (!Number.isFinite(refreshExpirySeconds) || refreshExpirySeconds <= 0) {
|
||||
throw new Error('refreshExpiryMs must be a positive duration for OpenID marker cookies');
|
||||
}
|
||||
|
||||
const signedUserId = jwt.sign({ id: userId }, secret, {
|
||||
expiresIn: refreshExpiryMs / 1000,
|
||||
expiresIn: refreshExpirySeconds,
|
||||
});
|
||||
res.cookie(OPENID_USER_ID_COOKIE, signedUserId, cookieOptions);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Model } from 'mongoose';
|
||||
import type { FilterQuery, Model, UpdateQuery } from 'mongoose';
|
||||
import type {
|
||||
IRefreshTokenBridge,
|
||||
RefreshTokenBridgeCreateData,
|
||||
|
|
@ -10,7 +10,7 @@ function bridgeFilter({
|
|||
oldRefreshTokenHash,
|
||||
userId,
|
||||
tenantId,
|
||||
}: RefreshTokenBridgeQuery): Record<string, unknown> {
|
||||
}: RefreshTokenBridgeQuery): FilterQuery<IRefreshTokenBridge> {
|
||||
return {
|
||||
oldRefreshTokenHash,
|
||||
userId,
|
||||
|
|
@ -30,14 +30,11 @@ export function createRefreshTokenBridgeMethods(mongoose: typeof import('mongoos
|
|||
try {
|
||||
const RefreshTokenBridge = mongoose.models.RefreshTokenBridge as Model<IRefreshTokenBridge>;
|
||||
const filter = bridgeFilter(bridgeData);
|
||||
const update: {
|
||||
$set: Record<string, unknown>;
|
||||
$setOnInsert: Record<string, unknown>;
|
||||
$unset?: Record<string, string>;
|
||||
} = {
|
||||
const update: UpdateQuery<IRefreshTokenBridge> = {
|
||||
$set: {
|
||||
encryptedNewRefreshToken: bridgeData.encryptedNewRefreshToken,
|
||||
expiresAt: bridgeData.expiresAt,
|
||||
...(bridgeData.openidIssuer != null && { openidIssuer: bridgeData.openidIssuer }),
|
||||
},
|
||||
$setOnInsert: {
|
||||
oldRefreshTokenHash: bridgeData.oldRefreshTokenHash,
|
||||
|
|
@ -45,12 +42,8 @@ export function createRefreshTokenBridgeMethods(mongoose: typeof import('mongoos
|
|||
...(bridgeData.tenantId != null && { tenantId: bridgeData.tenantId }),
|
||||
createdAt: new Date(),
|
||||
},
|
||||
...(bridgeData.openidIssuer == null && { $unset: { openidIssuer: '' } }),
|
||||
};
|
||||
if (bridgeData.openidIssuer != null) {
|
||||
update.$set.openidIssuer = bridgeData.openidIssuer;
|
||||
} else {
|
||||
update.$unset = { openidIssuer: '' };
|
||||
}
|
||||
return await RefreshTokenBridge.findOneAndUpdate(filter, update, {
|
||||
upsert: true,
|
||||
new: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue