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 actually like to approve is "send this draft, to these four people, once." The distance between those two sentences is where most of the anxiety about agents really lives — and closing it is a solved problem that almost nobody uses.

What a scope can and can't say

A scope is a string naming a class of capability. mail.send. payments.write. repo.admin. It's a good abstraction for the world it was designed for, where the client was a fixed program doing a known job and the interesting 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 to an agent is not approving a payment, they're approving all future payments, in advance, sight unseen, for the lifetime of the grant. That's not a consent decision anyone can make 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 cannot express the right-hand column in a scope string, and you 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 settling it means one payment, of that amount, from that account. That planning step is the agent's mission, and this page is where its output hits the wire.

Two consequences worth holding onto while reading the rest of this page.

Authorization details are the wire format for a decomposed mission. That's the useful way to think about RAR here. 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. A plan that collapses into payments.write on its way out has discarded everything the decomposition knew.

The decomposition is not trustworthy, and that's fine. It was produced by a component reading untrusted invoices. Which is exactly why it travels as a request: the user sees what was decomposed before approving it, and the resource compares what arrives against what was granted. An attacker who corrupts the planning step gets a request that a human declines or a resource rejects — not a payment. The value of putting the plan on the wire in a structured form is that it becomes reviewable by parties that don't trust the planner.

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, gets rendered 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, not the category. The user sees the payee and the amount.
  • The token is narrow by construction. It authorizes this payment. It cannot be reused for a different one, because the details are inside it.
  • The resource enforces exactly. It compares the incoming request to the authorization details in the token. A mismatch is a rejection, not a judgement call.
  • The audit record is the act. The log doesn't say "the agent used its payments scope." It says what was authorized, by whom, and when.
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 a request the resource rejects for not matching the token. The agent's judgement stops being the last line of defense.

The flow

Sharp-edged consent

Authorization request carrying authorization details
  1. AgentAuthorization Server

    Pushes the request with PAR. This matters more here than anywhere else: authorization details can be large, and they must not be modifiable in a browser between the agent and the consent screen. The server returns a request_uri.

  2. Authorization ServerUser

    Renders consent from the pushed request — payee, amount, date, source account — rather than from a scope name.

  3. Authorization ServerAgent

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

  4. AgentPayments 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 and consent screens nobody will finish. The useful split:

  • Scopes for standing, low-consequence capability. Reading a calendar. Listing files. Things where the category genuinely is the decision.
  • Authorization details for the sharp edges. Money moving, data leaving, things being deleted, anything irreversible. The set is usually small, and it's the set 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 an authorization server how to render a type it has never seen, or what to say when a client sends details it doesn't understand. Precision the user can't read isn't consent, and a rejection the client can't interpret isn't a protocol — it's a support ticket.

That's exactly what RAR Metadata and Error Remediation is for: letting an authorization server advertise which authorization details types and fields it supports, and return structured errors that tell the client what to fix. It's an adopted working group document, and it's the piece that turns RAR from something you can implement bilaterally into something an agent can negotiate with a stranger.

Until it lands, RAR works best where the type is agreed in advance — which is most enterprise and financial deployments, and increasingly the high-consequence tools an agent is given.