OAuth·for·Agents

Scenario 01 · adds the human

The agent acts for a person

Same agent, one change: now it's reading your calendar. The API suddenly has to answer two different questions on every request — which software is asking, and whose data it may touch — and the whole design of this rung is about keeping those two questions apart.

Two questions, two answers

In scenario 00 the agent was the subject. The token said sub: "ticket-summarizer" and that was the whole story. Add a person and the token has to carry both facts:

{
  "sub": "user:9f3c21",        // whose data — the resource owner
  "client_id": "calendar-agent",  // which software — the client
  "scope": "calendar.read",
  "aud": "https://cal.example.com/",
  "cnf": { "jkt": "0ZcOCORZ..." }
}

This is the thing OAuth was built to do, and it is worth saying plainly: the reason a compromised agent doesn't become a compromised company is that its token names one user and one capability. Collapse the two questions into one — an agent with a service account that can read everyone's calendar, and a user_id query parameter it promises to set correctly — and you've built an authorization system whose only enforcement point is the agent's own good behaviour.

The shortcut to avoid

"The agent has admin access and passes the user ID along" is the single most common design in production agent systems today. It is fast to build and it moves every authorization decision into the least trustworthy component in the system — the one taking instructions from natural language.

First, a classification question

Is your agent a client, or is it the user's own software?

Before picking a flow, decide what the agent is. OAuth has always drawn a line between software the user runs (a browser) and software a third party runs (an app that wants your data). Agents land on both sides of that line, and putting one on the wrong side is where most designs go wrong.

The agent is a third-party client

A hosted assistant, run by a vendor, that wants access to a user's calendar. This is exactly the case OAuth was designed for. It gets a client ID, the user sees a consent screen naming that vendor, and the token it holds is narrow and revocable.

The agent is the user's own agent

A CLI on the user's laptop, or software inside the user's own trust domain. It's closer to a browser: the user isn't being protected from it. It still needs an identity and still gets scoped tokens — but the consent story is about the resources it reaches, not about trusting the software.

The failure mode is the middle case: an agent that is hosted by a vendor but presented as the user's own. If the user's consent screen doesn't name the party that will actually hold the token, the user hasn't consented to what's happening.

Getting consent

Three ways to reach a human

The authorization code flow with PKCE is the default and, when the agent has a browser available, it is simply the right answer. Agents often don't. There are two standard alternatives, and picking between the three is a question about where the user's attention is.

The user is right here, with a browser

Authorization code + PKCE (RFC 7636, mandatory in OAuth 2.1). Push the request parameters ahead of time with PAR so the authorization request can't be tampered with in the browser, and so the consent screen renders from a request the authorization server received directly from the agent.

The agent has no browser — a terminal, a server, a chat window

The device authorization grant. The agent shows a short code and a URL; the user approves on whatever device they do have; the agent polls. Note the response it polls with: authorization_pending is a documented, expected, non-error state to sit in — the protocol has always had a place for "waiting for a human."

You know who the user is and can reach them out-of-band

CIBA — the agent names the user, the authorization server pushes an approval prompt to their phone, and no redirect happens at all. This becomes the backbone of scenario 04, where the human is genuinely absent rather than merely elsewhere.

Cross-device caution

Both the device grant and CIBA move approval to a different device from the one making the request, which is also the shape of a good phishing attack. RFC 10027 is the best current practice for getting these right — read it before you ship one.

The flow

Headless agent, user on their phone

Device authorization grant, DPoP-bound
  1. AgentAuthorization Server

    Asks for a device code, naming the API it wants and the scope it needs. Sends a DPoP proof, so the token it eventually gets will be bound to its key.

  2. Authorization ServerAgent

    Returns a device code, a short user code, and a verification URL.

  3. AgentUser

    Displays the URL and code. The user opens it on their phone, signs in, and sees a consent screen naming the agent and the access it's asking for.

  4. AgentAuthorization Server

    Polls the token endpoint. Gets authorization_pending until the user finishes; backs off if told to slow_down.

  5. Authorization ServerAgent

    Once approved: a DPoP-bound access token whose subject is the user, whose client is the agent — and a refresh token, also bound to the agent's key.

  6. AgentCalendar API

    Reads the user's calendar. The API's log names both the person and the software.

POST /device_authorization HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwiandr...

client_id=calendar-agent
&scope=calendar.read
&resource=https%3A%2F%2Fcal.example.com%2F

→ 200 OK
{
  "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://auth.example.com/device",
  "expires_in": 900,
  "interval": 5
}
POST /token HTTP/1.1        // polled every 5 seconds

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=calendar-agent

→ 400 Bad Request
{ "error": "authorization_pending" }   // not a failure. a state.

Long-running

Staying authorized without staying dangerous

An agent that runs for weeks needs refresh tokens, and a refresh token is the longest-lived credential in the system. Three things keep that from being the new API key:

  • Bind it to the key. A DPoP-bound refresh token is as useless to a thief as the access token. This is the single highest-value line of configuration on this page.
  • Rotate it. Each use issues a new one and invalidates the old. Replay of a used refresh token is a detectable signal that something was stolen, and the correct response is to kill the whole grant.
  • Give the whole grant an end date. The refresh token and authorization expiration draft lets the authorization server tell the client when the underlying authorization itself expires — not just this token. An agent that knows its grant ends in 30 days can ask for renewal at a sensible moment instead of failing at 3am.
And when the agent isn't the client

Sometimes the software holding the token is a service that runs agents, not the agent itself. Then the token should say so: Token Exchange's act claim names the actor operating on the subject's behalf, and it nests. That's the mechanism scenarios 05 and 06 lean on — it's introduced here because the moment there is both a person and an agent, "who is acting" becomes a claim you want written down rather than inferred.