| import { logger } from '@librechat/data-schemas'; | |
| import type { IUser } from '@librechat/data-schemas'; | |
| export interface OpenIDTokenInfo { | |
| accessToken?: string; | |
| idToken?: string; | |
| expiresAt?: number; | |
| userId?: string; | |
| userEmail?: string; | |
| userName?: string; | |
| claims?: Record<string, unknown>; | |
| } | |
| interface FederatedTokens { | |
| access_token?: string; | |
| id_token?: string; | |
| refresh_token?: string; | |
| expires_at?: number; | |
| } | |
| function isFederatedTokens(obj: unknown): obj is FederatedTokens { | |
| if (!obj || typeof obj !== 'object') { | |
| return false; | |
| } | |
| return 'access_token' in obj || 'id_token' in obj || 'expires_at' in obj; | |
| } | |
| const OPENID_TOKEN_FIELDS = [ | |
| 'ACCESS_TOKEN', | |
| 'ID_TOKEN', | |
| 'USER_ID', | |
| 'USER_EMAIL', | |
| 'USER_NAME', | |
| 'EXPIRES_AT', | |
| ] as const; | |
| export function extractOpenIDTokenInfo(user: IUser | null | undefined): OpenIDTokenInfo | null { | |
| if (!user) { | |
| return null; | |
| } | |
| try { | |
| if (user.provider !== 'openid' && !user.openidId) { | |
| return null; | |
| } | |
| const tokenInfo: OpenIDTokenInfo = {}; | |
| if ('federatedTokens' in user && isFederatedTokens(user.federatedTokens)) { | |
| const tokens = user.federatedTokens; | |
| logger.debug('[extractOpenIDTokenInfo] Found federatedTokens:', { | |
| has_access_token: !!tokens.access_token, | |
| has_id_token: !!tokens.id_token, | |
| has_refresh_token: !!tokens.refresh_token, | |
| expires_at: tokens.expires_at, | |
| }); | |
| tokenInfo.accessToken = tokens.access_token; | |
| tokenInfo.idToken = tokens.id_token; | |
| tokenInfo.expiresAt = tokens.expires_at; | |
| } else if ('openidTokens' in user && isFederatedTokens(user.openidTokens)) { | |
| const tokens = user.openidTokens; | |
| logger.debug('[extractOpenIDTokenInfo] Found openidTokens'); | |
| tokenInfo.accessToken = tokens.access_token; | |
| tokenInfo.idToken = tokens.id_token; | |
| tokenInfo.expiresAt = tokens.expires_at; | |
| } | |
| tokenInfo.userId = user.openidId || user.id; | |
| tokenInfo.userEmail = user.email; | |
| tokenInfo.userName = user.name || user.username; | |
| if (tokenInfo.idToken) { | |
| try { | |
| const payload = JSON.parse( | |
| Buffer.from(tokenInfo.idToken.split('.')[1], 'base64').toString(), | |
| ); | |
| tokenInfo.claims = payload; | |
| if (payload.sub) tokenInfo.userId = payload.sub; | |
| if (payload.email) tokenInfo.userEmail = payload.email; | |
| if (payload.name) tokenInfo.userName = payload.name; | |
| if (payload.exp) tokenInfo.expiresAt = payload.exp; | |
| } catch (jwtError) { | |
| logger.warn('Could not parse ID token claims:', jwtError); | |
| } | |
| } | |
| return tokenInfo; | |
| } catch (error) { | |
| logger.error('Error extracting OpenID token info:', error); | |
| return null; | |
| } | |
| } | |
| export function isOpenIDTokenValid(tokenInfo: OpenIDTokenInfo | null): boolean { | |
| if (!tokenInfo || !tokenInfo.accessToken) { | |
| return false; | |
| } | |
| if (tokenInfo.expiresAt) { | |
| const now = Math.floor(Date.now() / 1000); | |
| if (now >= tokenInfo.expiresAt) { | |
| logger.warn('OpenID token has expired'); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| export function processOpenIDPlaceholders( | |
| value: string, | |
| tokenInfo: OpenIDTokenInfo | null, | |
| ): string { | |
| if (!tokenInfo || typeof value !== 'string') { | |
| return value; | |
| } | |
| let processedValue = value; | |
| for (const field of OPENID_TOKEN_FIELDS) { | |
| const placeholder = `{{LIBRECHAT_OPENID_${field}}}`; | |
| if (!processedValue.includes(placeholder)) { | |
| continue; | |
| } | |
| let replacementValue = ''; | |
| switch (field) { | |
| case 'ACCESS_TOKEN': | |
| replacementValue = tokenInfo.accessToken || ''; | |
| break; | |
| case 'ID_TOKEN': | |
| replacementValue = tokenInfo.idToken || ''; | |
| break; | |
| case 'USER_ID': | |
| replacementValue = tokenInfo.userId || ''; | |
| break; | |
| case 'USER_EMAIL': | |
| replacementValue = tokenInfo.userEmail || ''; | |
| break; | |
| case 'USER_NAME': | |
| replacementValue = tokenInfo.userName || ''; | |
| break; | |
| case 'EXPIRES_AT': | |
| replacementValue = tokenInfo.expiresAt ? String(tokenInfo.expiresAt) : ''; | |
| break; | |
| } | |
| processedValue = processedValue.replace(new RegExp(placeholder, 'g'), replacementValue); | |
| } | |
| const genericPlaceholder = '{{LIBRECHAT_OPENID_TOKEN}}'; | |
| if (processedValue.includes(genericPlaceholder)) { | |
| const replacementValue = tokenInfo.accessToken || ''; | |
| processedValue = processedValue.replace(new RegExp(genericPlaceholder, 'g'), replacementValue); | |
| } | |
| return processedValue; | |
| } | |
| export function createBearerAuthHeader(tokenInfo: OpenIDTokenInfo | null): string { | |
| if (!tokenInfo || !tokenInfo.accessToken) { | |
| return ''; | |
| } | |
| return `Bearer ${tokenInfo.accessToken}`; | |
| } | |
| export function isOpenIDAvailable(): boolean { | |
| const openidClientId = process.env.OPENID_CLIENT_ID; | |
| const openidClientSecret = process.env.OPENID_CLIENT_SECRET; | |
| const openidIssuer = process.env.OPENID_ISSUER; | |
| return !!(openidClientId && openidClientSecret && openidIssuer); | |
| } | |