mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
* fix: update @librechat/agents dependency to version 3.0.29 * chore: fix typing by replacing TUser with IUser * chore: import order * fix: replace TUser with IUser in run and OAuthReconnectionManager modules * fix: update @librechat/agents dependency to version 3.0.30
194 lines
5.4 KiB
TypeScript
194 lines
5.4 KiB
TypeScript
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) {
|
|
logger.debug('[extractOpenIDTokenInfo] No user provided');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
logger.debug(
|
|
'[extractOpenIDTokenInfo] User provider:',
|
|
user.provider,
|
|
'openidId:',
|
|
user.openidId,
|
|
);
|
|
|
|
if (user.provider !== 'openid' && !user.openidId) {
|
|
logger.debug('[extractOpenIDTokenInfo] User not authenticated via OpenID');
|
|
return null;
|
|
}
|
|
|
|
const tokenInfo: OpenIDTokenInfo = {};
|
|
|
|
logger.debug(
|
|
'[extractOpenIDTokenInfo] Checking for federatedTokens in user object:',
|
|
'federatedTokens' in user,
|
|
);
|
|
|
|
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;
|
|
} else {
|
|
logger.warn(
|
|
'[extractOpenIDTokenInfo] No federatedTokens or openidTokens found in user object',
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|