OAuth·for·Agents

Scenario 04 · adds asynchronous approval

Nobody's watching and it needs permission

The agent has done nineteen things correctly and arrived at the twentieth: a refund above the auto-approve limit. Its user went to bed at eleven. Failing is wrong, proceeding is worse. It needs to wait, and for waiting to be something the protocol names.

Three wrong answers first

  • Fail and make the human start over. Forty minutes of context evaporates. This is what most systems do, and it's why "human in the loop" has a reputation for meaning "the agent wasn't useful."
  • Pre-approve everything at the start. Scenario 03's problem returning: the user approved a category months ago and has no visibility into the act.
  • Ask in the chat window. The agent prints "This refund is £900, shall I proceed?" and waits. By far the most common design in agent frameworks, and it has one fatal property.
The problem with in-band confirmation

The resource has no idea it happened. The refunds API sees a token with a refund scope and a valid request. Whether a human approved, or the "yes" came from text the agent read in a support ticket, isn't verifiable by the system enforcing the decision. The approval lives inside the least trustworthy component.

Approval has to be something the resource can demand and verify: it happens somewhere the resource trusts, and comes back as something it can check.

The shape that works

The resource challenges. A human answers. The token proves it.

OAuth already has this pattern for a weaker case. RFC 9470 lets a resource reject a request with insufficient_user_authentication and say what it needs: a fresher session, MFA. The resource sets the bar, the authorization server enforces it, the client relays.

// RFC 9470: the resource demands stronger authentication
→ 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication",
  error_description="Multi-factor authentication required",
  acr_values="urn:mace:incommon:iap:silver",
  max_age="300"

The OAuth Transaction Authorization Challenge draft points that pattern at the agent problem. Instead of "this user needs to authenticate harder," the resource says this specific operation needs approval and hands back a signed challenge describing it. The signature is what makes it work:

  • The authorization server can verify the challenge is real, so the agent didn't invent the operation being approved.
  • The human is shown the resource's description of the act, not the agent's.
  • The token that comes back refers to that challenge, so the resource can confirm the approval is the one it asked for.

The agent reduces to a courier: it carries a challenge it can't forge to a server it doesn't control, waits, and carries back a token whose contents it couldn't influence. Exactly as much trust as an agent should get.

The flow

Forty minutes in, at two in the morning

Transaction authorization challenge, resolved asynchronously
  1. Agent→Refunds API

    Requests a £900 refund with a perfectly valid token. The token is not the problem. The amount is.

  2. Refunds API→Agent

    401 with a signed challenge describing the operation: this refund, this order, this amount, this customer.

  3. Agent→Authorization Server

    Relays the challenge. It cannot alter it, because the signature won't survive.

  4. Authorization Server→Approver

    Verifies the resource's signature, works out who may approve (the user, their manager, whoever policy names), and pushes a prompt. CIBA's job, or a device-grant code, or an internal approvals queue.

  5. Agent→Authorization Server

    Polls, receiving authorization_pending for six hours if that's how long the human sleeps. Nothing sensitive is held during the wait.

  6. Authorization Server→Agent

    Approved at 07:14. The agent gets a token whose authorization_details describe this refund and nothing else, valid for two minutes.

  7. Agent→Refunds API

    Retries. The API checks the token's details against the challenge it issued and processes the refund, logging who approved it, when, and what they were shown.

// step 2: the challenge
→ 401 Unauthorized
WWW-Authenticate: Bearer error="transaction_authorization_required",
  challenge="eyJhbGciOiJFUzI1NiIsInR5cCI6ImNoYWxsZW5nZStqd3QifQ..."

// decoded payload, signed by the resource
{
  "iss": "https://api.shop.example/refunds",
  "aud": "https://auth.shop.example",
  "jti": "chal_01K9M2X7",
  "exp": 1787980000,
  "authorization_details": [{
    "type": "refund",
    "order_id": "ORD-88213",
    "amount": { "currency": "GBP", "amount": "900.00" },
    "customer": "cus_4Q1ZR",
    "reason": "damaged_on_arrival"
  }]
}
// step 5: waiting is a state, not an error
POST /token HTTP/1.1

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atxn-challenge
&challenge=eyJhbGciOiJFUzI1NiIsInR5cCI6ImNoYWxsZW5nZStqd3QifQ...

→ 400 Bad Request
{ "error": "authorization_pending", "interval": 30 }

The transaction challenge draft is an individual submission from June 2026, not yet a working group document, so parameter names will move. The pattern is the durable part, and it's the same one RFC 9470 and RFC 8628 already established.

Designing the interruption

The protocol is the easy half. The judgement calls:

  • The resource decides, not the agent. If the agent chooses when to ask, an attacker who controls its input chooses when to ask. Thresholds belong on the server side of the API.
  • Approve operations, not sessions. A challenge that resolves into "the agent may do refunds for an hour" threw away everything this rung bought. One challenge, one act.
  • Make it idempotent. The agent may retry, crash and resume, or run twice. Use the challenge identifier as the idempotency key so an approved refund can't become two.
  • Expire the challenge, generously. Minutes is too short for a sleeping human; forever is an approval sitting in someone's notifications for a task that finished last week. Hours.
  • Show the approver the resource's words. The one component whose description of the act they should not be reading is the agent's.
Why this rung is the interesting one

Everything below this point restricts what an agent can do. This is the first rung that makes an agent more capable: it can be trusted with longer, more consequential work because there's a verifiable way for it to stop and ask. Guardrails that hold are what let you take your hands off.