LibreChat/packages/api/src/utils/oidc.ts
Danny Avila 1814c81888
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
🕸️ fix: Minor Type Issues & Anthropic Web Search (#10618)
* 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
2025-11-21 14:25:05 -05:00

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);
}