danny-avila/LibreChat · error · Error
Graph token acquisition failed: ${error.message}
Error message
Graph token acquisition failed: ${error.message} What it means
getGraphApiToken() in GraphTokenService.js:22 wraps any failure of exchangeOboToken and rethrows with this message, preserving the original error.message. The wrapper exists to add Graph-specific context and log the openidId. The underlying cause is almost always an Entra/Azure AD OBO (jwt-bearer) exchange failure: bad assertion, missing consent, wrong scopes, or a transient IdP error.
Source
Thrown at api/server/services/GraphTokenService.js:22
/**
* Get Microsoft Graph API token using the On-Behalf-Of flow.
* Thin wrapper around the generic OBO exchange for Graph-specific error context.
*
* @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=true] - 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 {
return await exchangeOboToken(user, accessToken, scopes, fromCache);
} 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,
};
View on GitHub (pinned to 5ff282f900)
Solutions
- Read the wrapped error.message — it carries the IdP's reason (invalid_grant, consent_required, etc.).
- If invalid_grant/expired token: force a fresh sign-in so a new access token is minted, then retry.
- If consent_required: grant admin consent for the requested Graph scopes in the Entra app registration.
- Verify OPENID_ISSUER, client id/secret, and OBO scopes in librechat.yaml match the app registration.
Defensive patterns
Strategy: try-catch
Validate before calling
function assertOboInputs(user, accessToken, scopes) {
if (!user?.openidId) throw new Error('openidId required');
if (!accessToken) throw new Error('accessToken required');
if (!scopes) throw new Error('scopes required');
} Type guard
const hasOboInputs = (user, token, scopes) => !!user?.openidId && !!token && !!scopes;
Try / catch
try {
const token = await getGraphApiToken(user, accessToken, scopes);
} catch (e) {
if (/invalid_grant|expired/.test(e.message)) { await refreshUserSession(); return retry(); }
if (/consent_required/.test(e.message)) { return promptAdminConsent(scopes); }
throw e;
} Prevention
- Verify Entra app registration scopes and admin consent before enabling Graph-backed features.
- Log the wrapped error.message — it carries the IdP's specific reason.
- Confirm librechat.yaml OPENID settings match the app registration after each env change.
When it happens
Trigger: An MCP tool or Graph-dependent flow calls getGraphApiToken; exchangeOboToken rejects because the user's access token is expired, the requested scopes are not consented, the IdP returned 401/invalid_grant, or a transient 429/503 exhausted its single retry.
Common situations: OpenID/OBO misconfigured in librechat.yaml (wrong tenant, missing client secret). User lacks admin consent for the downstream scopes. Token cache served a stale token and the IdP rejected it. Network blip hitting the one retry budget.
Related errors
- User must be authenticated via OpenID to perform OBO token e
- [MCP][${serverName}][${toolName}] upstream authentication fa
- [MCP][${serverName}][${toolName}] OAuth authentication requi
- Failed to obtain SharePoint access token
- Request failed with status ${response.status}: ${json.error.
AI-assisted analysis of danny-avila/LibreChat@5ff282f900 (2026-08-12).
Data as JSON: /api/errors/09e938f910105596.
Report an issue: GitHub.