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

  1. Read the wrapped error.message — it carries the IdP's reason (invalid_grant, consent_required, etc.).
  2. If invalid_grant/expired token: force a fresh sign-in so a new access token is minted, then retry.
  3. If consent_required: grant admin consent for the requested Graph scopes in the Entra app registration.
  4. 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

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


AI-assisted analysis of danny-avila/LibreChat@5ff282f900 (2026-08-12). Data as JSON: /api/errors/09e938f910105596. Report an issue: GitHub.