{"record":{"id":"96432b3b8996cd86","repo":"different-ai/openwork","slug":"mcp-oauth-authorization-id-required","errorCode":"MCP_OAUTH_AUTHORIZATION_ID_REQUIRED","errorMessage":"A signed authorization transaction id is required before starting OAuth.","messagePattern":"A signed authorization transaction id is required before starting OAuth\\.","errorType":"exception","errorClass":"EnterpriseMcpOAuthContractError","httpStatus":null,"severity":"error","filePath":"packages/enterprise-mcp-client/src/oauth-provider.ts","lineNumber":140,"sourceCode":"      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_LIFECYCLE_DEADLINE\",\n        \"The enterprise MCP lifecycle expired before OAuth persistence could continue.\",\n      )\n    }\n    return {\n      connectionId: this.connectionId,\n      commitExpiresAt: this.lifecycle.expiresAt,\n      signal: this.lifecycle.signal,\n    }\n  }\n\n  get redirectUrl(): string {\n    return this.redirectUri\n  }\n\n  state(): string {\n    if (this.flow.kind !== \"connect\" || !this.flow.authorizationId) {\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_OAUTH_AUTHORIZATION_ID_REQUIRED\",\n        \"A signed authorization transaction id is required before starting OAuth.\",\n      )\n    }\n    return this.flow.authorizationId\n  }\n\n  get clientMetadata() {\n    const scope = this.requestedScopes.join(\" \")\n    return {\n      redirect_uris: [this.redirectUri],\n      client_name: this.clientName,\n      grant_types: [\"authorization_code\", \"refresh_token\"],\n      response_types: [\"code\"],\n      token_endpoint_auth_method: \"none\",\n      application_type: this.applicationType,\n      ...(scope ? { scope } : {}),\n    }","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/packages/enterprise-mcp-client/src/oauth-provider.ts#L122-L158","documentation":"state() is the OAuthClientProvider hook that supplies the OAuth state parameter. It requires the current flow to be a \"connect\" flow carrying a signed authorizationId; in callback or runtime flows (or a connect flow missing the id) it throws EnterpriseMcpOAuthContractError with code MCP_OAUTH_AUTHORIZATION_ID_REQUIRED. The signed id is what binds the browser redirect to a durable authorization transaction; without it the library will not start OAuth.","triggerScenarios":"The MCP SDK's auth() helper reads provider.state() while the provider was constructed with flow { kind: \"runtime\" } or { kind: \"callback\" }, or with a connect flow whose authorizationId is undefined — e.g. reusing a runtime session's provider to initiate a new OAuth redirect.","commonSituations":"Calling runtime operations (listTools/callTool) against a server that answers 401 and triggers an inline OAuth start without a signed id; calling connect() without authorizationId (see the connect-time guard) via a path that skips it; constructing the provider manually with the wrong flow kind.","solutions":["Start OAuth only via client.connect() with a signed authorizationId — do not rely on runtime operations to initiate OAuth.","Handle connect()'s { status: \"needs_auth\" } result to run the browser redirect with the properly signed state.","Ensure the authorizationId is passed and non-empty when constructing a connect flow provider.","If a 401 arrives during runtime calls, invalidate credentials and route the user back through connect() rather than re-authenticating in place."],"exampleFix":"// before (initiating OAuth from a runtime session)\nawait auth(runtimeSession.oauthProvider, { serverUrl })\n// after\nconst result = await client.connect({ connection, redirectUri, authorizationId: signedId })\nif (result.status === \"needs_auth\") redirect(result.authorizeUrl)","handlingStrategy":"validation","validationCode":"function canStartOAuth(authorizationId) {\n  return typeof authorizationId === \"string\" && authorizationId.length > 0;\n}\n// require this before initiating any OAuth redirect","typeGuard":"function isConnectFlowWithAuthorizationId(flow: { kind: string; authorizationId?: string }): flow is { kind: \"connect\"; authorizationId: string } {\n  return flow.kind === \"connect\" && typeof flow.authorizationId === \"string\" && flow.authorizationId.length > 0;\n}","tryCatchPattern":"try {\n  await client.callTool(input);\n} catch (e) {\n  if (e.code === \"MCP_OAUTH_AUTHORIZATION_ID_REQUIRED\") {\n    // a 401 started OAuth outside a signed connect flow — reroute via connect() with a signed id\n  }\n  throw e;\n}","preventionTips":["Always initiate OAuth through client.connect() with a signed authorizationId.","Handle { status: \"needs_auth\" } from connect() to run the redirect instead of letting runtime calls trigger auth.","Construct providers with the correct flow kind for their use (connect vs runtime).","On runtime 401s, invalidate credentials and restart via connect() rather than in-place re-auth."],"tags":["oauth","state-parameter","flow-lifecycle"],"backgroundTag":"missing-oauth-authorization-id","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}