LibreChat/packages/api/src/auth/openid.spec.ts
Danny Avila bcec5bfceb
🆔 fix: Prioritize Immutable Sub Claim for OIDC User ID (#9788)
* add use of immutable claims to identify user object

* fix semicolons

* update email attribute on change

* replace ternary expressions

* fix semicolon

* chore: add typing

* chore: reorder fields in `findOpenIDUser`

* refactor: optimize user lookup logic in `findOpenIDUser` function to minimize database roundtrips

* refactor: integrate findOpenIDUser for improved user retrieval in refreshController

* refactor: improve error logging for invalid refresh tokens in refreshController

* ci: mock findUser correctly in openidStrategy tests

* test: add unit tests for findOpenIDUser function to enhance user retrieval logic

---------

Co-authored-by: Joachim Keltsch <joachim.keltsch@daimlertruck.com>
2025-09-23 14:46:53 -04:00

408 lines
11 KiB
TypeScript

import { ErrorTypes } from 'librechat-data-provider';
import { logger } from '@librechat/data-schemas';
import type { IUser, UserMethods } from '@librechat/data-schemas';
import { findOpenIDUser } from './openid';
jest.mock('@librechat/data-schemas', () => ({
...jest.requireActual('@librechat/data-schemas'),
logger: {
warn: jest.fn(),
info: jest.fn(),
},
}));
describe('findOpenIDUser', () => {
let mockFindUser: jest.MockedFunction<UserMethods['findUser']>;
beforeEach(() => {
mockFindUser = jest.fn();
jest.clearAllMocks();
(logger.warn as jest.Mock).mockClear();
(logger.info as jest.Mock).mockClear();
});
describe('Primary condition searches', () => {
it('should find user by openidId', async () => {
const mockUser: IUser = {
_id: 'user123',
provider: 'openid',
openidId: 'openid_123',
email: 'user@example.com',
username: 'testuser',
} as IUser;
mockFindUser.mockResolvedValueOnce(mockUser);
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(mockFindUser).toHaveBeenCalledWith({
$or: [{ openidId: 'openid_123' }],
});
expect(result).toEqual({
user: mockUser,
error: null,
migration: false,
});
});
it('should find user by idOnTheSource', async () => {
const mockUser: IUser = {
_id: 'user123',
provider: 'openid',
idOnTheSource: 'source_123',
email: 'user@example.com',
username: 'testuser',
} as IUser;
mockFindUser.mockResolvedValueOnce(mockUser);
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
idOnTheSource: 'source_123',
});
expect(mockFindUser).toHaveBeenCalledWith({
$or: [{ openidId: 'openid_123' }, { idOnTheSource: 'source_123' }],
});
expect(result).toEqual({
user: mockUser,
error: null,
migration: false,
});
});
it('should find user by both openidId and idOnTheSource', async () => {
const mockUser: IUser = {
_id: 'user123',
provider: 'openid',
openidId: 'openid_123',
idOnTheSource: 'source_123',
email: 'user@example.com',
username: 'testuser',
} as IUser;
mockFindUser.mockResolvedValueOnce(mockUser);
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
idOnTheSource: 'source_123',
email: 'user@example.com',
});
expect(mockFindUser).toHaveBeenCalledWith({
$or: [{ openidId: 'openid_123' }, { idOnTheSource: 'source_123' }],
});
expect(result).toEqual({
user: mockUser,
error: null,
migration: false,
});
});
});
describe('Email-based searches', () => {
it('should find user by email when primary conditions fail', async () => {
const mockUser: IUser = {
_id: 'user123',
provider: 'openid',
openidId: 'openid_456',
email: 'user@example.com',
username: 'testuser',
} as IUser;
mockFindUser
.mockResolvedValueOnce(null) // Primary condition fails
.mockResolvedValueOnce(mockUser); // Email search succeeds
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(mockFindUser).toHaveBeenNthCalledWith(1, {
$or: [{ openidId: 'openid_123' }],
});
expect(mockFindUser).toHaveBeenNthCalledWith(2, { email: 'user@example.com' });
expect(result).toEqual({
user: mockUser,
error: null,
migration: false,
});
});
it('should return null user when email is not found', async () => {
mockFindUser
.mockResolvedValueOnce(null) // Primary condition fails
.mockResolvedValueOnce(null); // Email search fails
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(mockFindUser).toHaveBeenCalledTimes(2);
expect(result).toEqual({
user: null,
error: null,
migration: false,
});
});
it('should not search by email if not provided', async () => {
mockFindUser.mockResolvedValueOnce(null);
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
});
expect(mockFindUser).toHaveBeenCalledTimes(1);
expect(mockFindUser).toHaveBeenCalledWith({
$or: [{ openidId: 'openid_123' }],
});
expect(result).toEqual({
user: null,
error: null,
migration: false,
});
});
});
describe('Provider conflict handling', () => {
it('should return error when user has different provider', async () => {
const mockUser: IUser = {
_id: 'user123',
provider: 'google',
email: 'user@example.com',
username: 'testuser',
} as IUser;
mockFindUser
.mockResolvedValueOnce(null) // Primary condition fails
.mockResolvedValueOnce(mockUser); // Email search finds user with different provider
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(result).toEqual({
user: null,
error: ErrorTypes.AUTH_FAILED,
migration: false,
});
});
it('should allow login when user has openid provider', async () => {
const mockUser: IUser = {
_id: 'user123',
provider: 'openid',
openidId: 'openid_456',
email: 'user@example.com',
username: 'testuser',
} as IUser;
mockFindUser
.mockResolvedValueOnce(null) // Primary condition fails
.mockResolvedValueOnce(mockUser); // Email search finds user with openid provider
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(result).toEqual({
user: mockUser,
error: null,
migration: false,
});
});
});
describe('User migration scenarios', () => {
it('should prepare user for migration when email exists without openidId', async () => {
const mockUser: IUser = {
_id: 'user123',
email: 'user@example.com',
username: 'testuser',
// No provider and no openidId - needs migration
} as IUser;
mockFindUser
.mockResolvedValueOnce(null) // Primary condition fails
.mockResolvedValueOnce(mockUser); // Email search finds user without openidId
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(result).toEqual({
user: {
...mockUser,
provider: 'openid',
openidId: 'openid_123',
},
error: null,
migration: true,
});
});
it('should not migrate user who already has openidId', async () => {
const mockUser: IUser = {
_id: 'user123',
provider: 'openid',
openidId: 'existing_openid',
email: 'user@example.com',
username: 'testuser',
} as IUser;
mockFindUser
.mockResolvedValueOnce(null) // Primary condition fails
.mockResolvedValueOnce(mockUser); // Email search finds user with existing openidId
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(result).toEqual({
user: mockUser,
error: null,
migration: false,
});
});
it('should handle user with no provider but existing openidId', async () => {
const mockUser: IUser = {
_id: 'user123',
openidId: 'existing_openid',
email: 'user@example.com',
username: 'testuser',
// No provider field
} as IUser;
mockFindUser
.mockResolvedValueOnce(null) // Primary condition fails
.mockResolvedValueOnce(mockUser); // Email search finds user
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(result).toEqual({
user: mockUser,
error: null,
migration: false,
});
});
});
describe('Custom strategy names', () => {
it('should use custom strategy name in logs', async () => {
const loggerWarn = logger.warn as jest.Mock;
loggerWarn.mockClear();
mockFindUser.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
strategyName: 'customStrategy',
});
expect(loggerWarn).toHaveBeenCalledWith(expect.stringContaining('[customStrategy]'));
});
it('should default to openid strategy name', async () => {
const loggerWarn = logger.warn as jest.Mock;
loggerWarn.mockClear();
mockFindUser.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
email: 'user@example.com',
});
expect(loggerWarn).toHaveBeenCalledWith(expect.stringContaining('[openid]'));
});
});
describe('Edge cases', () => {
it('should handle empty string openidId', async () => {
mockFindUser.mockResolvedValueOnce(null);
const result = await findOpenIDUser({
openidId: '',
findUser: mockFindUser,
});
expect(mockFindUser).not.toHaveBeenCalled();
expect(result).toEqual({
user: null,
error: null,
migration: false,
});
});
it('should handle empty string idOnTheSource', async () => {
mockFindUser.mockResolvedValueOnce(null);
const result = await findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
idOnTheSource: '',
});
expect(mockFindUser).toHaveBeenCalledWith({
$or: [{ openidId: 'openid_123' }],
});
expect(result).toEqual({
user: null,
error: null,
migration: false,
});
});
it('should handle both openidId and idOnTheSource as empty strings', async () => {
await findOpenIDUser({
openidId: '',
findUser: mockFindUser,
idOnTheSource: '',
email: 'user@example.com',
});
// Should skip primary search and go directly to email search
expect(mockFindUser).toHaveBeenCalledTimes(1);
expect(mockFindUser).toHaveBeenCalledWith({ email: 'user@example.com' });
});
it('should handle findUser throwing an error', async () => {
mockFindUser.mockRejectedValueOnce(new Error('Database error'));
await expect(
findOpenIDUser({
openidId: 'openid_123',
findUser: mockFindUser,
}),
).rejects.toThrow('Database error');
});
});
});