danny-avila/LibreChat · error · Error

User must be authenticated via OpenID to perform OBO token e

Error message

User must be authenticated via OpenID to perform OBO token exchange

What it means

exchangeOboToken() in OboTokenService.js:128 throws this as its first validation guard: the OBO (jwt-bearer) flow requires an OpenID-authenticated user, identified by user.openidId. Without an openidId the service cannot build the cache key or assert the federated identity to the IdP, so it refuses immediately — before checking accessToken or scopes.

Source

Thrown at api/server/services/OboTokenService.js:128

/**
 * Exchange a user's access token for a downstream-scoped token via the
 * OAuth 2.0 On-Behalf-Of (jwt-bearer) grant.
 *
 * Concurrent callers for the same `${openidId}:${scopes}` key share a single
 * upstream exchange (see `inFlightExchanges`) so a fan-out of tool calls right
 * after a cache miss does not produce N parallel requests to the IdP.
 *
 * @param {Object} user - User object with OpenID information
 * @param {string} accessToken - Federated access token used as OBO assertion
 * @param {string} scopes - Scopes to request for the downstream service
 * @param {boolean} [fromCache=true] - When true, read from cache and join any
 *   in-flight exchange. When false, bypass both and force a fresh exchange.
 * @returns {Promise<Object>} Token response with access_token and expires_in
 */
async function exchangeOboToken(user, accessToken, scopes, fromCache = true) {
  if (!user.openidId) {
    throw new Error('User must be authenticated via OpenID to perform OBO token exchange');
  }

  if (!accessToken) {
    throw new Error('Access token is required for OBO exchange');
  }

  if (!scopes) {
    throw new Error('Scopes are required for OBO 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);

View on GitHub (pinned to 5ff282f900)

Solutions

  1. Ensure the user is authenticated via the OpenID strategy before invoking any OBO-dependent path.
  2. Verify the openidId claim is mapped onto req.user in the OpenID strategy callback.
  3. Gate the calling feature on user.openidId presence and surface a friendly 'sign in with SSO' message otherwise.

Example fix

// before
const token = await getGraphApiToken(req.user, accessToken, scopes);
// after
if (!req.user?.openidId) {
  return res.status(403).json({ error: 'Sign in with OpenID SSO to use this feature.' });
}
const token = await getGraphApiToken(req.user, accessToken, scopes);
Defensive patterns

Strategy: validation

Validate before calling

function assertOpenIdUser(user) {
  if (!user?.openidId) throw new Error('OpenID-authenticated user required for OBO');
}

Type guard

const isOpenIdUser = (u) => !!u?.openidId;

Prevention

When it happens

Trigger: Any caller invokes exchangeOboToken (directly or via getGraphApiToken) with a user object whose openidId is undefined/null. Typically a session that authenticated through a non-OpenID strategy (local/HEADER_AUTH) but the code path assumed OpenID.

Common situations: A feature gated on OpenID was enabled for a user who logged in via local auth. The user object was built from a JWT that lacked the openidId claim after a config change. Mixed-auth deployment where some sessions are OpenID and some are not, and the Graph/MCP path was hit by the wrong one.

Understand the failure class

Related errors


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