{"record":{"id":"c3457a8e745cd172","repo":"different-ai/openwork","slug":"mcp-oauth-authorization-missing","errorCode":"MCP_OAUTH_AUTHORIZATION_MISSING","errorMessage":"The OAuth authorization transaction is missing or was already consumed.","messagePattern":"The OAuth authorization transaction is missing or was already consumed\\.","errorType":"exception","errorClass":"EnterpriseMcpOAuthContractError","httpStatus":null,"severity":"error","filePath":"packages/enterprise-mcp-client/src/oauth-provider.ts","lineNumber":456,"sourceCode":"      codeVerifier,\n      expiresAt,\n      clientRegistrationRevision: this.loadedClient?.revision,\n    })\n  }\n\n  async codeVerifier(): Promise<string> {\n    if (this.flow.kind !== \"callback\") {\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_OAUTH_AUTHORIZATION_ID_REQUIRED\",\n        \"The OAuth callback is missing its signed authorization transaction id.\",\n      )\n    }\n    const transaction = await this.persistence.authorizations.load({\n      context: this.context(),\n      id: this.flow.authorizationId,\n    })\n    if (!transaction) {\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_OAUTH_AUTHORIZATION_MISSING\",\n        \"The OAuth authorization transaction is missing or was already consumed.\",\n      )\n    }\n    if (transaction.handle.expiresAt <= this.clock.now() + this.expirationSkewMs) {\n      await this.persistence.authorizations.invalidate({\n        context: this.context(),\n        id: this.flow.authorizationId,\n        reason: \"expired\",\n      })\n      throw new EnterpriseMcpOAuthContractError(\n        \"MCP_OAUTH_AUTHORIZATION_EXPIRED\",\n        \"The OAuth authorization transaction has expired; start the connection again.\",\n      )\n    }\n    const clientRevision = this.loadedClient?.revision\n    if (\n      transaction.handle.clientRegistrationRevision !== undefined","sourceCodeStart":438,"sourceCodeEnd":474,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/packages/enterprise-mcp-client/src/oauth-provider.ts#L438-L474","documentation":"Thrown from codeVerifier when the signed authorization id is present but no matching transaction can be loaded from the authorizations persistence. The transaction was either never persisted, already consumed by a completed exchange, or deleted/expired-and-invalidated. This protects against replaying a used or nonexistent authorization.","triggerScenarios":"codeVerifier() loads from persistence.authorizations and gets undefined — e.g. a second callback with the same state (replay), TTL cleanup removed the record, or a different persistence backend than the one used at begin().","commonSituations":"User refreshing the callback page after token exchange already consumed the transaction; sharing one persistence instance per-process while the flow crosses processes; server restart clearing an in-memory authorizations store mid-flow.","solutions":["Treat as replay/stale callback: restart the connect flow to mint a new transaction.","Use a shared, durable persistence backend for authorizations when the flow spans processes or restarts.","Deduplicate callbacks (single-use state handling) so a double callback does not race the first exchange."],"exampleFix":"// before — in-memory store lost on restart\nnew InMemoryAuthorizationsStore()\n// after\nnew RedisAuthorizationsStore(redis, { ttlMs: authTransactionTtlMs })","handlingStrategy":"try-catch","validationCode":"// Pre-check the transaction before exchange\nconst tx = await persistence.authorizations.load({ context, id: authorizationId })\nconst usable = Boolean(tx) && tx.handle.expiresAt > Date.now()","typeGuard":null,"tryCatchPattern":"try { const verifier = await provider.codeVerifier() }\ncatch (e) {\n  if (e instanceof EnterpriseMcpOAuthContractError && e.code === \"MCP_OAUTH_AUTHORIZATION_MISSING\") {\n    return restartConnectFlow() // consumed/replay/lost record\n  }\n  throw e\n}","preventionTips":["Mark callbacks as single-use and ignore duplicate callback hits","Use a durable, shared persistence backend for authorizations across restarts","Keep the same persistence instance configured for the entire flow lifetime"],"tags":["oauth","pkce","persistence","replay"],"backgroundTag":"oauth-authorization-transaction-missing","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}