| import { describe, it, expect, jest, beforeEach } from '@jest/globals'; |
| import { |
| extractOpenIDTokenInfo, |
| processOpenIDPlaceholders, |
| isOpenIDTokenValid, |
| createBearerAuthHeader, |
| isOpenIDAvailable, |
| type OpenIDTokenInfo, |
| } from '../packages/api/src/utils/oidc'; |
| import { processMCPEnv, resolveHeaders } from '../packages/api/src/utils/env'; |
| import type { TUser } from 'librechat-data-provider'; |
| import type { IUser } from '@librechat/data-schemas'; |
|
|
| |
| jest.mock('@librechat/data-schemas', () => ({ |
| logger: { |
| error: jest.fn(), |
| warn: jest.fn(), |
| info: jest.fn(), |
| }, |
| })); |
|
|
| describe('OpenID Connect Federated Provider Token Integration', () => { |
| |
| const mockCognitoUser: Partial<IUser> = { |
| id: 'user-123', |
| email: 'test@example.com', |
| name: 'Test User', |
| provider: 'openid', |
| openidId: 'cognito-user-123', |
| federatedTokens: { |
| access_token: 'cognito-access-token-123', |
| id_token: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjb2duaXRvLXVzZXItMTIzIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmFtZSI6IlRlc3QgVXNlciIsImV4cCI6MTcwMDAwMDAwMH0.fake-signature', |
| expires_at: Math.floor(Date.now() / 1000) + 3600, |
| }, |
| }; |
|
|
| const mockExpiredCognitoUser: Partial<IUser> = { |
| ...mockCognitoUser, |
| federatedTokens: { |
| access_token: 'expired-cognito-token', |
| id_token: 'expired-cognito-id-token', |
| expires_at: Math.floor(Date.now() / 1000) - 3600, |
| }, |
| }; |
|
|
| |
| const mockOpenIDTokensUser: Partial<IUser> = { |
| id: 'user-456', |
| email: 'alt@example.com', |
| name: 'Alt User', |
| provider: 'openid', |
| openidId: 'alt-user-456', |
| openidTokens: { |
| access_token: 'alt-access-token-456', |
| id_token: 'alt-id-token-789', |
| expires_at: Math.floor(Date.now() / 1000) + 3600, |
| }, |
| }; |
|
|
| beforeEach(() => { |
| jest.clearAllMocks(); |
| }); |
|
|
| describe('extractOpenIDTokenInfo', () => { |
| it('should extract federated provider token info from Cognito user', () => { |
| const tokenInfo = extractOpenIDTokenInfo(mockCognitoUser as IUser); |
|
|
| expect(tokenInfo).toEqual({ |
| accessToken: 'cognito-access-token-123', |
| idToken: expect.stringContaining('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9'), |
| expiresAt: expect.any(Number), |
| userId: 'cognito-user-123', |
| userEmail: 'test@example.com', |
| userName: 'Test User', |
| claims: expect.objectContaining({ |
| sub: 'cognito-user-123', |
| email: 'test@example.com', |
| name: 'Test User', |
| }), |
| }); |
| }); |
|
|
| it('should extract tokens from alternative storage location', () => { |
| const tokenInfo = extractOpenIDTokenInfo(mockOpenIDTokensUser as IUser); |
|
|
| expect(tokenInfo).toEqual({ |
| accessToken: 'alt-access-token-456', |
| idToken: 'alt-id-token-789', |
| expiresAt: expect.any(Number), |
| userId: 'alt-user-456', |
| userEmail: 'alt@example.com', |
| userName: 'Alt User', |
| }); |
| }); |
|
|
| it('should return null for non-OpenID user', () => { |
| const nonOpenIDUser: Partial<IUser> = { |
| id: 'user-123', |
| provider: 'google', |
| email: 'test@example.com', |
| }; |
|
|
| const tokenInfo = extractOpenIDTokenInfo(nonOpenIDUser as IUser); |
| expect(tokenInfo).toBeNull(); |
| }); |
|
|
| it('should return null for null/undefined user', () => { |
| expect(extractOpenIDTokenInfo(null)).toBeNull(); |
| expect(extractOpenIDTokenInfo(undefined)).toBeNull(); |
| }); |
|
|
| it('should handle JWT parsing errors gracefully', () => { |
| const userWithMalformedJWT: Partial<IUser> = { |
| ...mockCognitoUser, |
| federatedTokens: { |
| access_token: 'valid-access-token', |
| id_token: 'malformed.jwt.token', |
| expires_at: Math.floor(Date.now() / 1000) + 3600, |
| }, |
| }; |
|
|
| const tokenInfo = extractOpenIDTokenInfo(userWithMalformedJWT as IUser); |
|
|
| expect(tokenInfo).toBeDefined(); |
| expect(tokenInfo?.accessToken).toBe('valid-access-token'); |
| expect(tokenInfo?.claims).toBeUndefined(); |
| }); |
| }); |
|
|
| describe('isOpenIDTokenValid', () => { |
| it('should return true for valid Cognito token', () => { |
| const tokenInfo = extractOpenIDTokenInfo(mockCognitoUser as IUser); |
| expect(isOpenIDTokenValid(tokenInfo)).toBe(true); |
| }); |
|
|
| it('should return false for expired Cognito token', () => { |
| const tokenInfo = extractOpenIDTokenInfo(mockExpiredCognitoUser as IUser); |
| expect(isOpenIDTokenValid(tokenInfo)).toBe(false); |
| }); |
|
|
| it('should return false for null token info', () => { |
| expect(isOpenIDTokenValid(null)).toBe(false); |
| }); |
|
|
| it('should return false for token info without access token', () => { |
| const tokenInfo: OpenIDTokenInfo = { |
| userId: 'user-123', |
| userEmail: 'test@example.com', |
| }; |
| expect(isOpenIDTokenValid(tokenInfo)).toBe(false); |
| }); |
| }); |
|
|
| describe('processOpenIDPlaceholders', () => { |
| const tokenInfo: OpenIDTokenInfo = { |
| accessToken: 'cognito-access-token-123', |
| idToken: 'cognito-id-token-456', |
| userId: 'cognito-user-789', |
| userEmail: 'cognito@example.com', |
| userName: 'Cognito User', |
| expiresAt: 1700000000, |
| }; |
|
|
| it('should replace OpenID Connect token placeholders', () => { |
| const template = 'Bearer {{LIBRECHAT_OPENID_TOKEN}}'; |
| const result = processOpenIDPlaceholders(template, tokenInfo); |
| expect(result).toBe('Bearer cognito-access-token-123'); |
| }); |
|
|
| it('should replace specific OpenID Connect placeholders', () => { |
| const template = ` |
| Access: {{LIBRECHAT_OPENID_ACCESS_TOKEN}} |
| ID: {{LIBRECHAT_OPENID_ID_TOKEN}} |
| User: {{LIBRECHAT_OPENID_USER_ID}} |
| Email: {{LIBRECHAT_OPENID_USER_EMAIL}} |
| Name: {{LIBRECHAT_OPENID_USER_NAME}} |
| Expires: {{LIBRECHAT_OPENID_EXPIRES_AT}} |
| `; |
|
|
| const result = processOpenIDPlaceholders(template, tokenInfo); |
|
|
| expect(result).toContain('Access: cognito-access-token-123'); |
| expect(result).toContain('ID: cognito-id-token-456'); |
| expect(result).toContain('User: cognito-user-789'); |
| expect(result).toContain('Email: cognito@example.com'); |
| expect(result).toContain('Name: Cognito User'); |
| expect(result).toContain('Expires: 1700000000'); |
| }); |
|
|
| it('should handle missing token fields gracefully', () => { |
| const partialTokenInfo: OpenIDTokenInfo = { |
| accessToken: 'partial-cognito-token', |
| userId: 'user-123', |
| }; |
|
|
| const template = 'Token: {{LIBRECHAT_OPENID_TOKEN}}, Email: {{LIBRECHAT_OPENID_USER_EMAIL}}'; |
| const result = processOpenIDPlaceholders(template, partialTokenInfo); |
|
|
| expect(result).toBe('Token: partial-cognito-token, Email: '); |
| }); |
|
|
| it('should return original value for null token info', () => { |
| const template = 'Bearer {{LIBRECHAT_OPENID_TOKEN}}'; |
| const result = processOpenIDPlaceholders(template, null); |
| expect(result).toBe(template); |
| }); |
| }); |
|
|
| describe('createBearerAuthHeader', () => { |
| it('should create proper Bearer header with Cognito token', () => { |
| const tokenInfo: OpenIDTokenInfo = { |
| accessToken: 'cognito-test-token-123', |
| }; |
|
|
| const header = createBearerAuthHeader(tokenInfo); |
| expect(header).toBe('Bearer cognito-test-token-123'); |
| }); |
|
|
| it('should return empty string for null token info', () => { |
| const header = createBearerAuthHeader(null); |
| expect(header).toBe(''); |
| }); |
|
|
| it('should return empty string for token info without access token', () => { |
| const tokenInfo: OpenIDTokenInfo = { |
| userId: 'user-123', |
| }; |
|
|
| const header = createBearerAuthHeader(tokenInfo); |
| expect(header).toBe(''); |
| }); |
| }); |
|
|
| describe('isOpenIDAvailable', () => { |
| const originalEnv = process.env; |
|
|
| beforeEach(() => { |
| jest.resetModules(); |
| process.env = { ...originalEnv }; |
| }); |
|
|
| afterAll(() => { |
| process.env = originalEnv; |
| }); |
|
|
| it('should return true when OpenID Connect is properly configured for Cognito', () => { |
| process.env.OPENID_CLIENT_ID = 'cognito-client-id'; |
| process.env.OPENID_CLIENT_SECRET = 'cognito-client-secret'; |
| process.env.OPENID_ISSUER = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'; |
|
|
| expect(isOpenIDAvailable()).toBe(true); |
| }); |
|
|
| it('should return false when OpenID Connect is not configured', () => { |
| delete process.env.OPENID_CLIENT_ID; |
| delete process.env.OPENID_CLIENT_SECRET; |
| delete process.env.OPENID_ISSUER; |
|
|
| expect(isOpenIDAvailable()).toBe(false); |
| }); |
|
|
| it('should return false when OpenID Connect is partially configured', () => { |
| process.env.OPENID_CLIENT_ID = 'cognito-client-id'; |
| delete process.env.OPENID_CLIENT_SECRET; |
| delete process.env.OPENID_ISSUER; |
|
|
| expect(isOpenIDAvailable()).toBe(false); |
| }); |
| }); |
|
|
| describe('Integration with resolveHeaders', () => { |
| it('should resolve OpenID Connect placeholders in headers for Cognito', () => { |
| const headers = { |
| 'Authorization': '{{LIBRECHAT_OPENID_TOKEN}}', |
| 'X-User-ID': '{{LIBRECHAT_OPENID_USER_ID}}', |
| 'X-User-Email': '{{LIBRECHAT_OPENID_USER_EMAIL}}', |
| }; |
|
|
| const resolvedHeaders = resolveHeaders({ |
| headers, |
| user: mockCognitoUser as TUser, |
| }); |
|
|
| expect(resolvedHeaders['Authorization']).toBe('cognito-access-token-123'); |
| expect(resolvedHeaders['X-User-ID']).toBe('cognito-user-123'); |
| expect(resolvedHeaders['X-User-Email']).toBe('test@example.com'); |
| }); |
|
|
| it('should work with Bearer token format for Cognito', () => { |
| const headers = { |
| 'Authorization': 'Bearer {{LIBRECHAT_OPENID_TOKEN}}', |
| }; |
|
|
| const resolvedHeaders = resolveHeaders({ |
| headers, |
| user: mockCognitoUser as TUser, |
| }); |
|
|
| expect(resolvedHeaders['Authorization']).toBe('Bearer cognito-access-token-123'); |
| }); |
|
|
| it('should work with specific access token placeholder', () => { |
| const headers = { |
| 'Authorization': 'Bearer {{LIBRECHAT_OPENID_ACCESS_TOKEN}}', |
| 'X-Cognito-ID-Token': '{{LIBRECHAT_OPENID_ID_TOKEN}}', |
| }; |
|
|
| const resolvedHeaders = resolveHeaders({ |
| headers, |
| user: mockCognitoUser as TUser, |
| }); |
|
|
| expect(resolvedHeaders['Authorization']).toBe('Bearer cognito-access-token-123'); |
| expect(resolvedHeaders['X-Cognito-ID-Token']).toContain('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9'); |
| }); |
| }); |
|
|
| describe('Integration with processMCPEnv', () => { |
| it('should process OpenID Connect placeholders in MCP environment variables for Cognito', () => { |
| const mcpOptions = { |
| command: 'node', |
| args: ['server.js'], |
| env: { |
| 'COGNITO_ACCESS_TOKEN': '{{LIBRECHAT_OPENID_TOKEN}}', |
| 'USER_ID': '{{LIBRECHAT_OPENID_USER_ID}}', |
| 'USER_EMAIL': '{{LIBRECHAT_OPENID_USER_EMAIL}}', |
| }, |
| }; |
|
|
| const processedOptions = processMCPEnv({ |
| options: mcpOptions, |
| user: mockCognitoUser as TUser, |
| }); |
|
|
| expect(processedOptions.env?.['COGNITO_ACCESS_TOKEN']).toBe('cognito-access-token-123'); |
| expect(processedOptions.env?.['USER_ID']).toBe('cognito-user-123'); |
| expect(processedOptions.env?.['USER_EMAIL']).toBe('test@example.com'); |
| }); |
|
|
| it('should process OpenID Connect placeholders in MCP headers for HTTP transport', () => { |
| const mcpOptions = { |
| type: 'sse' as const, |
| url: 'https://api.example.com/mcp', |
| headers: { |
| 'Authorization': 'Bearer {{LIBRECHAT_OPENID_ACCESS_TOKEN}}', |
| 'X-Cognito-User-Info': '{{LIBRECHAT_OPENID_USER_EMAIL}}', |
| 'X-Cognito-ID-Token': '{{LIBRECHAT_OPENID_ID_TOKEN}}', |
| }, |
| }; |
|
|
| const processedOptions = processMCPEnv({ |
| options: mcpOptions, |
| user: mockCognitoUser as TUser, |
| }); |
|
|
| expect(processedOptions.headers?.['Authorization']).toBe('Bearer cognito-access-token-123'); |
| expect(processedOptions.headers?.['X-Cognito-User-Info']).toBe('test@example.com'); |
| expect(processedOptions.headers?.['X-Cognito-ID-Token']).toContain('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9'); |
| }); |
|
|
| it('should handle AWS-specific MCP server configuration', () => { |
| const awsMcpOptions = { |
| command: 'node', |
| args: ['aws-mcp-server.js'], |
| env: { |
| 'AWS_COGNITO_TOKEN': '{{LIBRECHAT_OPENID_ACCESS_TOKEN}}', |
| 'AWS_COGNITO_ID_TOKEN': '{{LIBRECHAT_OPENID_ID_TOKEN}}', |
| 'COGNITO_USER_SUB': '{{LIBRECHAT_OPENID_USER_ID}}', |
| }, |
| }; |
|
|
| const processedOptions = processMCPEnv({ |
| options: awsMcpOptions, |
| user: mockCognitoUser as TUser, |
| }); |
|
|
| expect(processedOptions.env?.['AWS_COGNITO_TOKEN']).toBe('cognito-access-token-123'); |
| expect(processedOptions.env?.['AWS_COGNITO_ID_TOKEN']).toContain('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9'); |
| expect(processedOptions.env?.['COGNITO_USER_SUB']).toBe('cognito-user-123'); |
| }); |
| }); |
|
|
| describe('Security and Edge Cases', () => { |
| it('should not process OpenID Connect placeholders for expired tokens', () => { |
| const headers = { |
| 'Authorization': 'Bearer {{LIBRECHAT_OPENID_TOKEN}}', |
| }; |
|
|
| const resolvedHeaders = resolveHeaders({ |
| headers, |
| user: mockExpiredCognitoUser as TUser, |
| }); |
|
|
| |
| expect(resolvedHeaders['Authorization']).toBe('Bearer {{LIBRECHAT_OPENID_TOKEN}}'); |
| }); |
|
|
| it('should handle malformed federated token data gracefully', () => { |
| const malformedUser: Partial<IUser> = { |
| id: 'user-123', |
| provider: 'openid', |
| openidId: 'cognito-user', |
| federatedTokens: null, |
| }; |
|
|
| const headers = { |
| 'Authorization': 'Bearer {{LIBRECHAT_OPENID_TOKEN}}', |
| }; |
|
|
| const resolvedHeaders = resolveHeaders({ |
| headers, |
| user: malformedUser as TUser, |
| }); |
|
|
| |
| expect(resolvedHeaders['Authorization']).toBe('Bearer {{LIBRECHAT_OPENID_TOKEN}}'); |
| }); |
|
|
| it('should handle multiple placeholder instances in same string', () => { |
| const template = '{{LIBRECHAT_OPENID_TOKEN}}-{{LIBRECHAT_OPENID_TOKEN}}-{{LIBRECHAT_OPENID_USER_ID}}'; |
|
|
| const tokenInfo: OpenIDTokenInfo = { |
| accessToken: 'cognito-token123', |
| userId: 'cognito-user456', |
| }; |
|
|
| const result = processOpenIDPlaceholders(template, tokenInfo); |
| expect(result).toBe('cognito-token123-cognito-token123-cognito-user456'); |
| }); |
|
|
| it('should handle users without federated tokens storage', () => { |
| const userWithoutTokens: Partial<IUser> = { |
| id: 'user-789', |
| provider: 'openid', |
| openidId: 'user-without-tokens', |
| email: 'no-tokens@example.com', |
| |
| }; |
|
|
| const headers = { |
| 'Authorization': 'Bearer {{LIBRECHAT_OPENID_TOKEN}}', |
| }; |
|
|
| const resolvedHeaders = resolveHeaders({ |
| headers, |
| user: userWithoutTokens as TUser, |
| }); |
|
|
| |
| expect(resolvedHeaders['Authorization']).toBe('Bearer {{LIBRECHAT_OPENID_TOKEN}}'); |
| }); |
|
|
| it('should prioritize federatedTokens over openidTokens', () => { |
| const userWithBothTokens: Partial<IUser> = { |
| id: 'user-priority', |
| provider: 'openid', |
| openidId: 'priority-user', |
| federatedTokens: { |
| access_token: 'federated-priority-token', |
| expires_at: Math.floor(Date.now() / 1000) + 3600, |
| }, |
| openidTokens: { |
| access_token: 'openid-fallback-token', |
| expires_at: Math.floor(Date.now() / 1000) + 3600, |
| }, |
| }; |
|
|
| const tokenInfo = extractOpenIDTokenInfo(userWithBothTokens as IUser); |
| expect(tokenInfo?.accessToken).toBe('federated-priority-token'); |
| }); |
| }); |
| }); |