{"record":{"id":"92e170aa7915afe4","repo":"danny-avila/LibreChat","slug":"user-must-be-authenticated-via-openid-to-perform-o","errorCode":null,"errorMessage":"User must be authenticated via OpenID to perform OBO token exchange","messagePattern":"User must be authenticated via OpenID to perform OBO token exchange","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"api/server/services/OboTokenService.js","lineNumber":128,"sourceCode":"\n/**\n * Exchange a user's access token for a downstream-scoped token via the\n * OAuth 2.0 On-Behalf-Of (jwt-bearer) grant.\n *\n * Concurrent callers for the same `${openidId}:${scopes}` key share a single\n * upstream exchange (see `inFlightExchanges`) so a fan-out of tool calls right\n * after a cache miss does not produce N parallel requests to the IdP.\n *\n * @param {Object} user - User object with OpenID information\n * @param {string} accessToken - Federated access token used as OBO assertion\n * @param {string} scopes - Scopes to request for the downstream service\n * @param {boolean} [fromCache=true] - When true, read from cache and join any\n *   in-flight exchange. When false, bypass both and force a fresh exchange.\n * @returns {Promise<Object>} Token response with access_token and expires_in\n */\nasync function exchangeOboToken(user, accessToken, scopes, fromCache = true) {\n  if (!user.openidId) {\n    throw new Error('User must be authenticated via OpenID to perform OBO token exchange');\n  }\n\n  if (!accessToken) {\n    throw new Error('Access token is required for OBO exchange');\n  }\n\n  if (!scopes) {\n    throw new Error('Scopes are required for OBO exchange');\n  }\n\n  const config = getOpenIdConfig();\n  if (!config) {\n    throw new Error('OpenID configuration not available');\n  }\n\n  const cacheKey = `${user.openidId}:${scopes}`;\n  const tokensCache = getLogStores(CacheKeys.OPENID_EXCHANGED_TOKENS);\n","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/danny-avila/LibreChat/blob/5ff282f9006c436e561de1afd39a481bea1ef0d8/api/server/services/OboTokenService.js#L110-L146","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the user is authenticated via the OpenID strategy before invoking any OBO-dependent path.","Verify the openidId claim is mapped onto req.user in the OpenID strategy callback.","Gate the calling feature on user.openidId presence and surface a friendly 'sign in with SSO' message otherwise."],"exampleFix":"// before\nconst token = await getGraphApiToken(req.user, accessToken, scopes);\n// after\nif (!req.user?.openidId) {\n  return res.status(403).json({ error: 'Sign in with OpenID SSO to use this feature.' });\n}\nconst token = await getGraphApiToken(req.user, accessToken, scopes);","handlingStrategy":"validation","validationCode":"function assertOpenIdUser(user) {\n  if (!user?.openidId) throw new Error('OpenID-authenticated user required for OBO');\n}","typeGuard":"const isOpenIdUser = (u) => !!u?.openidId;","tryCatchPattern":null,"preventionTips":["Gate OBO-dependent features on user.openidId presence.","Ensure the OpenID strategy maps the openidId claim onto the session user.","Refuse non-OpenID sessions at the route boundary, not inside the token service."],"tags":["openid","oauth","authentication","tokens","validation"],"backgroundTag":null,"analyzedSha":"5ff282f9006c436e561de1afd39a481bea1ef0d8","analyzedAt":"2026-08-12T21:38:08.145Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}