OAuth·for·Agents

Scenario 03 · adds precision

The agent says exactly what it's about to do

"This agent may send email on your behalf" is a promise with no edges. What a user would like to approve is "send this draft, to these four people, once." The distance between those sentences is where the anxiety about agents lives, and closing it is a solved problem almost nobody uses.

What a scope can and can't say

A scope names a class of capability: mail.send, payments.write, repo.admin. A good abstraction for the world it was designed for, where the client was a fixed program doing a known job and the question was which category of thing it did.

Agents invert that. The software is general-purpose; the action is the specific thing. A user granting payments.write isn't approving a payment, they're approving all future payments, in advance, sight unseen, for the lifetime of the grant. Nobody can make that decision well.

What the user is asked

"Finance Assistant would like to make payments from your account."

Approve once. Applies forever. Nobody can evaluate this.

What the user wants to be asked

"Finance Assistant wants to pay £2,400 to Ridgeway Supplies from your business current account, on 2 September."

Approve this. Bounded. Evaluable in two seconds.

You can't express the right-hand column in a scope string, and shouldn't try: a scope named pay_ridgeway_2400_gbp is a parameter smuggled through a namespace. There is a proper mechanism.

Where the details come from

Something upstream decomposed a sentence

Nobody handed the agent that payment. It was given an objective, "settle what we owe Ridgeway," and worked out that this means one payment, of that amount, from that account. That planning step is the agent's mission, and this is where its output hits the wire.

Authorization details are the wire format for a decomposed mission. The planning is application logic and no specification will standardize it, but the moment the plan leaves the agent it needs a shape, and a structured description of an operation is that shape.

The decomposition isn't trustworthy, and that's fine. It came from a component reading untrusted invoices, which is why it travels as a request: the user sees it before approving, and the resource checks what arrives against what was granted. Corrupting the planning step gets you a request a human declines, not a payment.

The mechanism

Rich Authorization Requests

RFC 9396 adds one parameter, authorization_details: a JSON array where each element has a type and whatever fields that type defines. It travels through the authorization request, renders on the consent screen, lands in the access token, and is enforced by the resource.

{
  "authorization_details": [
    {
      "type": "payment_initiation",
      "actions": ["initiate"],
      "instructedAmount": { "currency": "GBP", "amount": "2400.00" },
      "creditorName": "Ridgeway Supplies Ltd",
      "creditorAccount": { "iban": "GB33BUKB20201555555555" },
      "debtorAccount": { "iban": "GB94BARC10201530093459" },
      "requestedExecutionDate": "2026-09-02"
    }
  ]
}

Four things change once the request looks like this.

  • The consent screen can be honest. It renders the operation, so the user sees the payee and the amount.
  • The token is narrow by construction. It authorizes this payment and can't be reused for a different one, because the details are inside it.
  • The resource enforces exactly. It compares the incoming request to the token's authorization details. A mismatch is a rejection, not a judgement call.
  • The audit record is the act. The log says what was authorized, by whom, and when, not "the agent used its payments scope."
The agent-shaped benefit

Prompt injection stops being an authorization bypass. An attacker who convinces an agent to attempt a different payment gets a token request the user will see, or one the resource rejects for not matching the token. The agent's judgement is no longer the last line of defense.

The flow

Sharp-edged consent

Authorization request carrying authorization details
  1. Agent→Authorization Server

    Pushes the request with PAR and gets a request_uri. This matters more here than anywhere else: authorization details can be large, and nothing in a browser may modify them on the way to the consent screen.

  2. Authorization Server→User

    Renders consent from the pushed request: payee, amount, date, source account. Not from a scope name.

  3. Authorization Server→Agent

    User approves. The token response echoes the authorization_details that were actually granted, which may be narrower than what was asked for.

  4. Agent→Payments API

    Submits the payment. The API compares the request body against the details in the token and executes only if they match.

POST /par HTTP/1.1
Host: auth.bank.example
Content-Type: application/x-www-form-urlencoded

response_type=code
&client_id=https%3A%2F%2Fagent.example.com%2Fclients%2Ffinance-assistant
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&authorization_details=%5B%7B%22type%22%3A%22payment_initiation%22%2C...%5D

→ 201 Created
{
  "request_uri": "urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c",
  "expires_in": 90
}
// token response: note the granted details, echoed back
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
  "token_type": "DPoP",
  "expires_in": 120,
  "authorization_details": [
    {
      "type": "payment_initiation",
      "actions": ["initiate"],
      "instructedAmount": { "currency": "GBP", "amount": "2400.00" },
      "creditorAccount": { "iban": "GB33BUKB20201555555555" },
      "paymentId": "pmt_01K8Z3QW9F"          // enriched by the AS
    }
  ]
}

Don't put everything in it

RAR is not a replacement for scopes, and treating it as one produces authorization requests nobody can read. The useful split:

  • Scopes for standing, low-consequence capability. Reading a calendar, listing files, anything where the category genuinely is the decision.
  • Authorization details for the sharp edges. Money moving, data leaving, anything irreversible. A small set, and the one worth interrupting a human for.

A well-designed agent integration has a handful of RAR types and a normal set of scopes, not fifty types.

Honest about the edges

The part that isn't finished

RAR lets you express an operation precisely. It doesn't tell a server how to render a type it has never seen, or what to say about details it doesn't understand. Precision the user can't read isn't consent, and a rejection the client can't interpret is a support ticket.

That's what RAR Metadata and Error Remediation is for: letting a server advertise which types it supports and return structured errors telling the client what to fix. It turns RAR from something implemented bilaterally into something an agent negotiates with a stranger. Until then, RAR works best where the type is agreed in advance.