mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-15 15:08:10 +01:00
184 lines
5.5 KiB
JavaScript
184 lines
5.5 KiB
JavaScript
|
|
const { SystemRoles } = require('librechat-data-provider');
|
|||
|
|
|
|||
|
|
// --- Capture the verify callback from JwtStrategy ---
|
|||
|
|
let capturedVerifyCallback;
|
|||
|
|
jest.mock('passport-jwt', () => ({
|
|||
|
|
Strategy: jest.fn((_opts, verifyCallback) => {
|
|||
|
|
capturedVerifyCallback = verifyCallback;
|
|||
|
|
return { name: 'jwt' };
|
|||
|
|
}),
|
|||
|
|
ExtractJwt: {
|
|||
|
|
fromAuthHeaderAsBearerToken: jest.fn(() => 'mock-extractor'),
|
|||
|
|
},
|
|||
|
|
}));
|
|||
|
|
jest.mock('jwks-rsa', () => ({
|
|||
|
|
passportJwtSecret: jest.fn(() => 'mock-secret-provider'),
|
|||
|
|
}));
|
|||
|
|
jest.mock('https-proxy-agent', () => ({
|
|||
|
|
HttpsProxyAgent: jest.fn(),
|
|||
|
|
}));
|
|||
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|||
|
|
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
|
|||
|
|
}));
|
|||
|
|
jest.mock('@librechat/api', () => ({
|
|||
|
|
isEnabled: jest.fn(() => false),
|
|||
|
|
findOpenIDUser: jest.fn(),
|
|||
|
|
math: jest.fn((val, fallback) => fallback),
|
|||
|
|
}));
|
|||
|
|
jest.mock('~/models', () => ({
|
|||
|
|
findUser: jest.fn(),
|
|||
|
|
updateUser: jest.fn(),
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
const { findOpenIDUser } = require('@librechat/api');
|
|||
|
|
const { updateUser } = require('~/models');
|
|||
|
|
const openIdJwtLogin = require('./openIdJwtStrategy');
|
|||
|
|
|
|||
|
|
// Helper: build a mock openIdConfig
|
|||
|
|
const mockOpenIdConfig = {
|
|||
|
|
serverMetadata: () => ({ jwks_uri: 'https://example.com/.well-known/jwks.json' }),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Helper: invoke the captured verify callback
|
|||
|
|
async function invokeVerify(req, payload) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
capturedVerifyCallback(req, payload, (err, user, info) => {
|
|||
|
|
if (err) {
|
|||
|
|
return reject(err);
|
|||
|
|
}
|
|||
|
|
resolve({ user, info });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('openIdJwtStrategy – token source handling', () => {
|
|||
|
|
const baseUser = {
|
|||
|
|
_id: { toString: () => 'user-abc' },
|
|||
|
|
role: SystemRoles.USER,
|
|||
|
|
provider: 'openid',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const payload = { sub: 'oidc-123', email: 'test@example.com', exp: 9999999999 };
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
jest.clearAllMocks();
|
|||
|
|
findOpenIDUser.mockResolvedValue({ user: { ...baseUser }, error: null, migration: false });
|
|||
|
|
updateUser.mockResolvedValue({});
|
|||
|
|
|
|||
|
|
// Initialize the strategy so capturedVerifyCallback is set
|
|||
|
|
openIdJwtLogin(mockOpenIdConfig);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should read all tokens from session when available', async () => {
|
|||
|
|
const req = {
|
|||
|
|
headers: { authorization: 'Bearer raw-bearer-token' },
|
|||
|
|
session: {
|
|||
|
|
openidTokens: {
|
|||
|
|
accessToken: 'session-access',
|
|||
|
|
idToken: 'session-id',
|
|||
|
|
refreshToken: 'session-refresh',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const { user } = await invokeVerify(req, payload);
|
|||
|
|
|
|||
|
|
expect(user.federatedTokens).toEqual({
|
|||
|
|
access_token: 'session-access',
|
|||
|
|
id_token: 'session-id',
|
|||
|
|
refresh_token: 'session-refresh',
|
|||
|
|
expires_at: payload.exp,
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should fall back to cookies when session is absent', async () => {
|
|||
|
|
const req = {
|
|||
|
|
headers: {
|
|||
|
|
authorization: 'Bearer raw-bearer-token',
|
|||
|
|
cookie:
|
|||
|
|
'openid_access_token=cookie-access; openid_id_token=cookie-id; refreshToken=cookie-refresh',
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const { user } = await invokeVerify(req, payload);
|
|||
|
|
|
|||
|
|
expect(user.federatedTokens).toEqual({
|
|||
|
|
access_token: 'cookie-access',
|
|||
|
|
id_token: 'cookie-id',
|
|||
|
|
refresh_token: 'cookie-refresh',
|
|||
|
|
expires_at: payload.exp,
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should fall back to cookie for idToken only when session lacks it', async () => {
|
|||
|
|
const req = {
|
|||
|
|
headers: {
|
|||
|
|
authorization: 'Bearer raw-bearer-token',
|
|||
|
|
cookie: 'openid_id_token=cookie-id',
|
|||
|
|
},
|
|||
|
|
session: {
|
|||
|
|
openidTokens: {
|
|||
|
|
accessToken: 'session-access',
|
|||
|
|
// idToken intentionally missing
|
|||
|
|
refreshToken: 'session-refresh',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const { user } = await invokeVerify(req, payload);
|
|||
|
|
|
|||
|
|
expect(user.federatedTokens).toEqual({
|
|||
|
|
access_token: 'session-access',
|
|||
|
|
id_token: 'cookie-id',
|
|||
|
|
refresh_token: 'session-refresh',
|
|||
|
|
expires_at: payload.exp,
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should use raw Bearer token as access_token fallback when neither session nor cookie has one', async () => {
|
|||
|
|
const req = {
|
|||
|
|
headers: {
|
|||
|
|
authorization: 'Bearer raw-bearer-token',
|
|||
|
|
cookie: 'openid_id_token=cookie-id; refreshToken=cookie-refresh',
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const { user } = await invokeVerify(req, payload);
|
|||
|
|
|
|||
|
|
expect(user.federatedTokens.access_token).toBe('raw-bearer-token');
|
|||
|
|
expect(user.federatedTokens.id_token).toBe('cookie-id');
|
|||
|
|
expect(user.federatedTokens.refresh_token).toBe('cookie-refresh');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should set id_token to undefined when not available in session or cookies', async () => {
|
|||
|
|
const req = {
|
|||
|
|
headers: {
|
|||
|
|
authorization: 'Bearer raw-bearer-token',
|
|||
|
|
cookie: 'openid_access_token=cookie-access; refreshToken=cookie-refresh',
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const { user } = await invokeVerify(req, payload);
|
|||
|
|
|
|||
|
|
expect(user.federatedTokens.access_token).toBe('cookie-access');
|
|||
|
|
expect(user.federatedTokens.id_token).toBeUndefined();
|
|||
|
|
expect(user.federatedTokens.refresh_token).toBe('cookie-refresh');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should keep id_token and access_token as distinct values from cookies', async () => {
|
|||
|
|
const req = {
|
|||
|
|
headers: {
|
|||
|
|
authorization: 'Bearer raw-bearer-token',
|
|||
|
|
cookie:
|
|||
|
|
'openid_access_token=the-access-token; openid_id_token=the-id-token; refreshToken=the-refresh',
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const { user } = await invokeVerify(req, payload);
|
|||
|
|
|
|||
|
|
expect(user.federatedTokens.access_token).toBe('the-access-token');
|
|||
|
|
expect(user.federatedTokens.id_token).toBe('the-id-token');
|
|||
|
|
expect(user.federatedTokens.access_token).not.toBe(user.federatedTokens.id_token);
|
|||
|
|
});
|
|||
|
|
});
|