different-ai/openwork · error · EnterpriseMcpOAuthContractError

MCP_OAUTH_AUTHORIZATION_MISSING

MCP_OAUTH_AUTHORIZATION_MISSING

Error message

The OAuth authorization transaction is missing or was already consumed.

What it means

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.

Source

Thrown at packages/enterprise-mcp-client/src/oauth-provider.ts:456

      codeVerifier,
      expiresAt,
      clientRegistrationRevision: this.loadedClient?.revision,
    })
  }

  async codeVerifier(): Promise<string> {
    if (this.flow.kind !== "callback") {
      throw new EnterpriseMcpOAuthContractError(
        "MCP_OAUTH_AUTHORIZATION_ID_REQUIRED",
        "The OAuth callback is missing its signed authorization transaction id.",
      )
    }
    const transaction = await this.persistence.authorizations.load({
      context: this.context(),
      id: this.flow.authorizationId,
    })
    if (!transaction) {
      throw new EnterpriseMcpOAuthContractError(
        "MCP_OAUTH_AUTHORIZATION_MISSING",
        "The OAuth authorization transaction is missing or was already consumed.",
      )
    }
    if (transaction.handle.expiresAt <= this.clock.now() + this.expirationSkewMs) {
      await this.persistence.authorizations.invalidate({
        context: this.context(),
        id: this.flow.authorizationId,
        reason: "expired",
      })
      throw new EnterpriseMcpOAuthContractError(
        "MCP_OAUTH_AUTHORIZATION_EXPIRED",
        "The OAuth authorization transaction has expired; start the connection again.",
      )
    }
    const clientRevision = this.loadedClient?.revision
    if (
      transaction.handle.clientRegistrationRevision !== undefined

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Treat as replay/stale callback: restart the connect flow to mint a new transaction.
  2. Use a shared, durable persistence backend for authorizations when the flow spans processes or restarts.
  3. Deduplicate callbacks (single-use state handling) so a double callback does not race the first exchange.

Example fix

// before — in-memory store lost on restart
new InMemoryAuthorizationsStore()
// after
new RedisAuthorizationsStore(redis, { ttlMs: authTransactionTtlMs })
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check the transaction before exchange
const tx = await persistence.authorizations.load({ context, id: authorizationId })
const usable = Boolean(tx) && tx.handle.expiresAt > Date.now()

Try / catch

try { const verifier = await provider.codeVerifier() }
catch (e) {
  if (e instanceof EnterpriseMcpOAuthContractError && e.code === "MCP_OAUTH_AUTHORIZATION_MISSING") {
    return restartConnectFlow() // consumed/replay/lost record
  }
  throw e
}

Prevention

When it happens

Trigger: 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().

Common situations: 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.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/c3457a8e745cd172. Report an issue: GitHub.