{"record":{"id":"b46caf82f9e2a48f","repo":"vercel/ai","slug":"existing-oauth-client-information-is-required-when","errorCode":null,"errorMessage":"Existing OAuth client information is required when exchanging an authorization code","messagePattern":"Existing OAuth client information is required when exchanging an authorization code","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/mcp/src/tool/oauth.ts","lineNumber":1318,"sourceCode":"  /** Load or register client credentials with the AS pin attached. */\n  let clientInformation = await Promise.resolve(provider.clientInformation());\n  if (clientInformation?.issuer != null) {\n    const storedAuthorizationServerInformation =\n      await getStoredAuthorizationServerInformation({\n        provider,\n        clientInformation,\n      });\n    if (storedAuthorizationServerInformation) {\n      assertAuthorizationServerInformationMatches({\n        storedAuthorizationServerInformation,\n        currentAuthorizationServerInformation,\n      });\n    }\n  }\n\n  if (!clientInformation) {\n    if (authorizationCode !== undefined) {\n      throw new Error(\n        'Existing OAuth client information is required when exchanging an authorization code',\n      );\n    }\n\n    if (!provider.saveClientInformation) {\n      throw new Error(\n        'OAuth client information must be saveable for dynamic registration',\n      );\n    }\n\n    const fullInformation = await registerClient(authorizationServerUrl, {\n      metadata,\n      clientMetadata: {\n        ...clientMetadata,\n        scope: selectedScope,\n      },\n      fetchFn,\n    });","sourceCodeStart":1300,"sourceCodeEnd":1336,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/mcp/src/tool/oauth.ts#L1300-L1336","documentation":"In authInternal's resume/callback path, when an authorizationCode is present the client must exchange it at the token endpoint, which requires the client_id (and secret) obtained earlier via registration or supplied statically. If provider.clientInformation() returns nothing at this point, the state needed to complete the flow is missing and this error is thrown.","triggerScenarios":"Calling auth() with authorizationCode set (the OAuth redirect callback) while provider.clientInformation() returns undefined — typically because client information from the initial authorization step was never persisted or cannot be loaded.","commonSituations":"Multi-instance/serverless deployments where the first auth() call registered a client in-memory on one instance but the callback lands on another; a provider implementation lacking saveClientInformation persistence; storage reset between authorization and callback.","solutions":["Implement saveClientInformation/clientInformation in your OAuthClientProvider to durably persist the registered client info (database, KV store, encrypted cookie) so it survives across callback requests.","Alternatively supply static client credentials from clientInformation() so registration/persistence is never needed.","Check that the storage backend used between the two auth() calls isn't being cleared (serverless cold starts, in-memory maps).","Restart the flow from scratch (drop the stale authorizationCode and call auth() without it) if client state is unrecoverable."],"exampleFix":"// before: client info only in memory, lost before callback\nconst clients = new Map();\n// after: persist across callback\nasync saveClientInformation(clientInformation) {\n  await kv.set('oauth:client', JSON.stringify(clientInformation));\n}\nasync clientInformation() {\n  const raw = await kv.get('oauth:client');\n  return raw ? JSON.parse(raw) : undefined;\n}","handlingStrategy":"try-catch","validationCode":"const info = await provider.clientInformation();\nif (!info && isOAuthCallback(request)) {\n  throw new Error('Client information missing at callback; ensure saveClientInformation persisted it during the initial auth() call');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await auth(provider, { serverUrl, authorizationCode });\n} catch (error) {\n  if (String(error.message).includes('Existing OAuth client information is required')) {\n    // state lost between redirect and callback: restart the whole flow\n    await provider.invalidateCredentials?.('all');\n    return restartAuthorization(provider, serverUrl);\n  }\n  throw error;\n}","preventionTips":["Persist client information in a shared store (DB/KV), never in-process memory, especially on serverless.","Always implement both saveClientInformation and clientInformation symmetrically.","Or bypass dynamic registration entirely with static client credentials.","Test the full authorize→callback round-trip across process restarts before shipping."],"tags":["oauth","state-management","client-information","mcp"],"backgroundTag":"missing-oauth-client-information","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}