| | import { extractOpenIDTokenInfo, isOpenIDTokenValid, processOpenIDPlaceholders } from './oidc'; |
| | import type { TUser } from 'librechat-data-provider'; |
| |
|
| | describe('OpenID Token Utilities', () => { |
| | describe('extractOpenIDTokenInfo', () => { |
| | it('should extract token info from user with federatedTokens', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'openid', |
| | openidId: 'oidc-sub-456', |
| | email: 'test@example.com', |
| | name: 'Test User', |
| | federatedTokens: { |
| | access_token: 'access-token-value', |
| | id_token: 'id-token-value', |
| | refresh_token: 'refresh-token-value', |
| | expires_at: Math.floor(Date.now() / 1000) + 3600, |
| | }, |
| | }; |
| |
|
| | const result = extractOpenIDTokenInfo(user); |
| |
|
| | expect(result).toMatchObject({ |
| | accessToken: 'access-token-value', |
| | idToken: 'id-token-value', |
| | userId: expect.any(String), |
| | userEmail: 'test@example.com', |
| | userName: 'Test User', |
| | }); |
| | expect(result?.expiresAt).toBeDefined(); |
| | }); |
| |
|
| | it('should return null when user is undefined', () => { |
| | const result = extractOpenIDTokenInfo(undefined); |
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | it('should return null when user is not OpenID provider', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'email', |
| | }; |
| |
|
| | const result = extractOpenIDTokenInfo(user); |
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | it('should return token info when user has no federatedTokens but is OpenID provider', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'openid', |
| | openidId: 'oidc-sub-456', |
| | email: 'test@example.com', |
| | name: 'Test User', |
| | }; |
| |
|
| | const result = extractOpenIDTokenInfo(user); |
| |
|
| | expect(result).toMatchObject({ |
| | userId: 'oidc-sub-456', |
| | userEmail: 'test@example.com', |
| | userName: 'Test User', |
| | }); |
| | expect(result?.accessToken).toBeUndefined(); |
| | expect(result?.idToken).toBeUndefined(); |
| | }); |
| |
|
| | it('should extract partial token info when some tokens are missing', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'openid', |
| | openidId: 'oidc-sub-456', |
| | email: 'test@example.com', |
| | federatedTokens: { |
| | access_token: 'access-token-value', |
| | id_token: undefined, |
| | refresh_token: undefined, |
| | expires_at: undefined, |
| | }, |
| | }; |
| |
|
| | const result = extractOpenIDTokenInfo(user); |
| |
|
| | expect(result).toMatchObject({ |
| | accessToken: 'access-token-value', |
| | userId: 'oidc-sub-456', |
| | userEmail: 'test@example.com', |
| | }); |
| | }); |
| |
|
| | it('should prioritize openidId over regular id', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'openid', |
| | openidId: 'oidc-sub-456', |
| | federatedTokens: { |
| | access_token: 'access-token-value', |
| | }, |
| | }; |
| |
|
| | const result = extractOpenIDTokenInfo(user); |
| |
|
| | expect(result?.userId).toBe('oidc-sub-456'); |
| | }); |
| |
|
| | it('should fall back to regular id when openidId is not available', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'openid', |
| | federatedTokens: { |
| | access_token: 'access-token-value', |
| | }, |
| | }; |
| |
|
| | const result = extractOpenIDTokenInfo(user); |
| |
|
| | expect(result?.userId).toBe('user-123'); |
| | }); |
| | }); |
| |
|
| | describe('isOpenIDTokenValid', () => { |
| | it('should return false when tokenInfo is null', () => { |
| | expect(isOpenIDTokenValid(null)).toBe(false); |
| | }); |
| |
|
| | it('should return false when tokenInfo has no accessToken', () => { |
| | const tokenInfo = { |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | expect(isOpenIDTokenValid(tokenInfo)).toBe(false); |
| | }); |
| |
|
| | it('should return true when token has access token and no expiresAt', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | expect(isOpenIDTokenValid(tokenInfo)).toBe(true); |
| | }); |
| |
|
| | it('should return true when token has not expired', () => { |
| | const futureTimestamp = Math.floor(Date.now() / 1000) + 3600; |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | expiresAt: futureTimestamp, |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | expect(isOpenIDTokenValid(tokenInfo)).toBe(true); |
| | }); |
| |
|
| | it('should return false when token has expired', () => { |
| | const pastTimestamp = Math.floor(Date.now() / 1000) - 3600; |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | expiresAt: pastTimestamp, |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | expect(isOpenIDTokenValid(tokenInfo)).toBe(false); |
| | }); |
| |
|
| | it('should return false when token expires exactly now', () => { |
| | const nowTimestamp = Math.floor(Date.now() / 1000); |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | expiresAt: nowTimestamp, |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | expect(isOpenIDTokenValid(tokenInfo)).toBe(false); |
| | }); |
| |
|
| | it('should return true when token is just about to expire (within 1 second)', () => { |
| | const almostExpiredTimestamp = Math.floor(Date.now() / 1000) + 1; |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | expiresAt: almostExpiredTimestamp, |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | expect(isOpenIDTokenValid(tokenInfo)).toBe(true); |
| | }); |
| | }); |
| |
|
| | describe('processOpenIDPlaceholders', () => { |
| | it('should replace LIBRECHAT_OPENID_TOKEN with access token', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | idToken: 'id-token-value', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = 'Authorization: Bearer {{LIBRECHAT_OPENID_TOKEN}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('Authorization: Bearer access-token-value'); |
| | }); |
| |
|
| | it('should replace LIBRECHAT_OPENID_ACCESS_TOKEN with access token', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = 'Token: {{LIBRECHAT_OPENID_ACCESS_TOKEN}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('Token: access-token-value'); |
| | }); |
| |
|
| | it('should replace LIBRECHAT_OPENID_ID_TOKEN with id token', () => { |
| | const tokenInfo = { |
| | idToken: 'id-token-value', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = 'ID Token: {{LIBRECHAT_OPENID_ID_TOKEN}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('ID Token: id-token-value'); |
| | }); |
| |
|
| | it('should replace LIBRECHAT_OPENID_USER_ID with user id', () => { |
| | const tokenInfo = { |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = 'User: {{LIBRECHAT_OPENID_USER_ID}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('User: oidc-sub-456'); |
| | }); |
| |
|
| | it('should replace LIBRECHAT_OPENID_USER_EMAIL with user email', () => { |
| | const tokenInfo = { |
| | userEmail: 'test@example.com', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = 'Email: {{LIBRECHAT_OPENID_USER_EMAIL}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('Email: test@example.com'); |
| | }); |
| |
|
| | it('should replace LIBRECHAT_OPENID_USER_NAME with user name', () => { |
| | const tokenInfo = { |
| | userName: 'Test User', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = 'Name: {{LIBRECHAT_OPENID_USER_NAME}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('Name: Test User'); |
| | }); |
| |
|
| | it('should replace multiple placeholders in a single string', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | idToken: 'id-token-value', |
| | userId: 'oidc-sub-456', |
| | userEmail: 'test@example.com', |
| | }; |
| |
|
| | const input = |
| | 'Authorization: Bearer {{LIBRECHAT_OPENID_TOKEN}}, ID: {{LIBRECHAT_OPENID_ID_TOKEN}}, User: {{LIBRECHAT_OPENID_USER_ID}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe( |
| | 'Authorization: Bearer access-token-value, ID: id-token-value, User: oidc-sub-456', |
| | ); |
| | }); |
| |
|
| | it('should replace empty string when token field is undefined', () => { |
| | const tokenInfo = { |
| | accessToken: undefined, |
| | idToken: undefined, |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = |
| | 'Access: {{LIBRECHAT_OPENID_TOKEN}}, ID: {{LIBRECHAT_OPENID_ID_TOKEN}}, User: {{LIBRECHAT_OPENID_USER_ID}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('Access: , ID: , User: oidc-sub-456'); |
| | }); |
| |
|
| | it('should handle all placeholder types in one value', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | idToken: 'id-token-value', |
| | userId: 'oidc-sub-456', |
| | userEmail: 'test@example.com', |
| | userName: 'Test User', |
| | expiresAt: 1234567890, |
| | }; |
| |
|
| | const input = ` |
| | Authorization: Bearer {{LIBRECHAT_OPENID_TOKEN}} |
| | ID Token: {{LIBRECHAT_OPENID_ID_TOKEN}} |
| | Access Token (alt): {{LIBRECHAT_OPENID_ACCESS_TOKEN}} |
| | User ID: {{LIBRECHAT_OPENID_USER_ID}} |
| | User Email: {{LIBRECHAT_OPENID_USER_EMAIL}} |
| | User Name: {{LIBRECHAT_OPENID_USER_NAME}} |
| | Expires: {{LIBRECHAT_OPENID_EXPIRES_AT}} |
| | `; |
| |
|
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toContain('Bearer access-token-value'); |
| | expect(result).toContain('ID Token: id-token-value'); |
| | expect(result).toContain('Access Token (alt): access-token-value'); |
| | expect(result).toContain('User ID: oidc-sub-456'); |
| | expect(result).toContain('User Email: test@example.com'); |
| | expect(result).toContain('User Name: Test User'); |
| | expect(result).toContain('Expires: 1234567890'); |
| | }); |
| |
|
| | it('should not modify string when no placeholders are present', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = 'Authorization: Bearer static-token'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('Authorization: Bearer static-token'); |
| | }); |
| |
|
| | it('should handle case-sensitive placeholders', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | |
| | const input = 'Token: {{librechat_openid_token}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('Token: {{librechat_openid_token}}'); |
| | }); |
| |
|
| | it('should handle multiple occurrences of the same placeholder', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const input = |
| | 'Primary: {{LIBRECHAT_OPENID_TOKEN}}, Secondary: {{LIBRECHAT_OPENID_TOKEN}}, Backup: {{LIBRECHAT_OPENID_TOKEN}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe( |
| | 'Primary: access-token-value, Secondary: access-token-value, Backup: access-token-value', |
| | ); |
| | }); |
| |
|
| | it('should handle token info with all fields undefined except userId', () => { |
| | const tokenInfo = { |
| | accessToken: undefined, |
| | idToken: undefined, |
| | userId: 'oidc-sub-456', |
| | userEmail: undefined, |
| | userName: undefined, |
| | }; |
| |
|
| | const input = |
| | 'Access: {{LIBRECHAT_OPENID_TOKEN}}, ID: {{LIBRECHAT_OPENID_ID_TOKEN}}, User: {{LIBRECHAT_OPENID_USER_ID}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo); |
| |
|
| | expect(result).toBe('Access: , ID: , User: oidc-sub-456'); |
| | }); |
| |
|
| | it('should return original value when tokenInfo is null', () => { |
| | const input = 'Authorization: Bearer {{LIBRECHAT_OPENID_TOKEN}}'; |
| | const result = processOpenIDPlaceholders(input, null); |
| |
|
| | expect(result).toBe('Authorization: Bearer {{LIBRECHAT_OPENID_TOKEN}}'); |
| | }); |
| |
|
| | it('should return original value when value is not a string', () => { |
| | const tokenInfo = { |
| | accessToken: 'access-token-value', |
| | userId: 'oidc-sub-456', |
| | }; |
| |
|
| | const result = processOpenIDPlaceholders(123 as unknown as string, tokenInfo); |
| |
|
| | expect(result).toBe(123); |
| | }); |
| | }); |
| |
|
| | describe('Integration: Full OpenID Token Flow', () => { |
| | it('should extract, validate, and process tokens correctly', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'openid', |
| | openidId: 'oidc-sub-456', |
| | email: 'test@example.com', |
| | name: 'Test User', |
| | federatedTokens: { |
| | access_token: 'access-token-value', |
| | id_token: 'id-token-value', |
| | refresh_token: 'refresh-token-value', |
| | expires_at: Math.floor(Date.now() / 1000) + 3600, |
| | }, |
| | }; |
| |
|
| | |
| | const tokenInfo = extractOpenIDTokenInfo(user); |
| | expect(tokenInfo).not.toBeNull(); |
| |
|
| | |
| | const isValid = isOpenIDTokenValid(tokenInfo!); |
| | expect(isValid).toBe(true); |
| |
|
| | |
| | const input = |
| | 'Authorization: Bearer {{LIBRECHAT_OPENID_TOKEN}}, User: {{LIBRECHAT_OPENID_USER_ID}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo!); |
| | expect(result).toContain('Authorization: Bearer access-token-value'); |
| | expect(result).toContain('User:'); |
| | }); |
| |
|
| | it('should handle expired tokens correctly', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'openid', |
| | openidId: 'oidc-sub-456', |
| | federatedTokens: { |
| | access_token: 'access-token-value', |
| | expires_at: Math.floor(Date.now() / 1000) - 3600, |
| | }, |
| | }; |
| |
|
| | const tokenInfo = extractOpenIDTokenInfo(user); |
| | expect(tokenInfo).not.toBeNull(); |
| |
|
| | const isValid = isOpenIDTokenValid(tokenInfo!); |
| | expect(isValid).toBe(false); |
| |
|
| | |
| | |
| | const input = 'Authorization: Bearer {{LIBRECHAT_OPENID_TOKEN}}'; |
| | const result = processOpenIDPlaceholders(input, tokenInfo!); |
| | expect(result).toBe('Authorization: Bearer access-token-value'); |
| | }); |
| |
|
| | it('should handle user with no federatedTokens but still has OpenID provider', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'openid', |
| | openidId: 'oidc-sub-456', |
| | }; |
| |
|
| | const tokenInfo = extractOpenIDTokenInfo(user); |
| | expect(tokenInfo).not.toBeNull(); |
| | expect(tokenInfo?.userId).toBe('oidc-sub-456'); |
| | expect(tokenInfo?.accessToken).toBeUndefined(); |
| | }); |
| |
|
| | it('should handle missing user', () => { |
| | const tokenInfo = extractOpenIDTokenInfo(undefined); |
| | expect(tokenInfo).toBeNull(); |
| | }); |
| |
|
| | it('should handle non-OpenID users', () => { |
| | const user: Partial<TUser> = { |
| | id: 'user-123', |
| | provider: 'email', |
| | }; |
| |
|
| | const tokenInfo = extractOpenIDTokenInfo(user); |
| | expect(tokenInfo).toBeNull(); |
| | }); |
| | }); |
| | }); |
| |
|