LibreChat/api/server/services/GraphTokenService.js
Danny Avila cca9d63224
🔒 refactor: graphTokenController to use federated access token for OBO assertion (#11893)
- Removed the extraction of access token from the Authorization header.
- Implemented logic to use the federated access token from the user object.
- Added error handling for missing federated access token.
- Updated related documentation in GraphTokenService to reflect changes in access token usage.
- Introduced unit tests for various scenarios in AuthController.spec.js to ensure proper functionality.
2026-02-21 18:03:39 -05:00

86 lines
2.8 KiB
JavaScript

const client = require('openid-client');
const { logger } = require('@librechat/data-schemas');
const { CacheKeys } = require('librechat-data-provider');
const { getOpenIdConfig } = require('~/strategies/openidStrategy');
const getLogStores = require('~/cache/getLogStores');
/**
* Get Microsoft Graph API token using existing token exchange mechanism
* @param {Object} user - User object with OpenID information
* @param {string} accessToken - Federated access token used as OBO assertion
* @param {string} scopes - Graph API scopes for the token
* @param {boolean} fromCache - Whether to try getting token from cache first
* @returns {Promise<Object>} Graph API token response with access_token and expires_in
*/
async function getGraphApiToken(user, accessToken, scopes, fromCache = true) {
try {
if (!user.openidId) {
throw new Error('User must be authenticated via Entra ID to access Microsoft Graph');
}
if (!accessToken) {
throw new Error('Access token is required for token exchange');
}
if (!scopes) {
throw new Error('Graph API scopes are required for token exchange');
}
const config = getOpenIdConfig();
if (!config) {
throw new Error('OpenID configuration not available');
}
const cacheKey = `${user.openidId}:${scopes}`;
const tokensCache = getLogStores(CacheKeys.OPENID_EXCHANGED_TOKENS);
if (fromCache) {
const cachedToken = await tokensCache.get(cacheKey);
if (cachedToken) {
logger.debug(`[GraphTokenService] Using cached Graph API token for user: ${user.openidId}`);
return cachedToken;
}
}
logger.debug(`[GraphTokenService] Requesting new Graph API token for user: ${user.openidId}`);
logger.debug(`[GraphTokenService] Requested scopes: ${scopes}`);
const grantResponse = await client.genericGrantRequest(
config,
'urn:ietf:params:oauth:grant-type:jwt-bearer',
{
scope: scopes,
assertion: accessToken,
requested_token_use: 'on_behalf_of',
},
);
const tokenResponse = {
access_token: grantResponse.access_token,
token_type: 'Bearer',
expires_in: grantResponse.expires_in || 3600,
scope: scopes,
};
await tokensCache.set(
cacheKey,
tokenResponse,
(grantResponse.expires_in || 3600) * 1000, // Convert to milliseconds
);
logger.debug(
`[GraphTokenService] Successfully obtained and cached Graph API token for user: ${user.openidId}`,
);
return tokenResponse;
} catch (error) {
logger.error(
`[GraphTokenService] Failed to acquire Graph API token for user ${user.openidId}:`,
error,
);
throw new Error(`Graph token acquisition failed: ${error.message}`);
}
}
module.exports = {
getGraphApiToken,
};