Scenario 01 · adds the human
The agent acts for a person
Same agent, one change: now it's reading your calendar. The API has to answer two questions on every request, which software is asking and whose data it may touch, and this whole rung is about keeping them 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. A compromised agent doesn't become a compromised company, because its token names one user and one capability.
Collapse the two questions into one and you get an agent with a service account that can read everyone's
calendar, plus a user_id query parameter it promises to set correctly. The only enforcement
point left in your authorization system is the agent's own good behaviour.
"The agent has admin access and passes the user ID along" is the most common design in production today. It's fast to build, and it moves every authorization decision into the least trustworthy component, the one taking instructions from natural language.
First, a classification question
Is your agent a client, or is it the user's own software?
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, 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 a user's calendar. 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 in the user's own trust domain. Closer to a browser: the user isn't being protected from it. It still needs an identity and scoped tokens, but consent is about the resources it reaches, not the software.
The failure mode is the middle case: an agent hosted by a vendor but presented as the user's own. If the consent screen doesn't name the party that will hold the token, the user hasn't consented to what's happening.
Getting consent
Three ways to reach a human
Authorization code with PKCE is the default, and when the agent has a browser it's simply the right answer. Agents often don't. 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 parameters ahead of time with PAR so nothing in the browser can tamper with the request, and the consent screen renders from what the 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 have, and the agent polls.
authorization_pending is a documented, expected, non-error state: 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.
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. Read it before you ship one.
The flow
Headless agent, user on their phone
-
Agent→Authorization 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.
-
Authorization Server→Agent
Returns a device code, a short user code, and a verification URL.
-
Agent→User
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.
-
Agent→Authorization Server
Polls the token endpoint. Gets
authorization_pendinguntil the user finishes; backs off if told toslow_down. -
Authorization Server→Agent
Once approved: a DPoP-bound access token whose subject is the user and whose client is the agent. Plus a refresh token, also bound to the agent's key.
-
Agent→Calendar 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, 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. The 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 means 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 server say when the underlying authorization expires, not just this token, so an agent can renew at a sensible moment instead of failing at 3am.
Sometimes the software holding the token is a service that runs agents, not the agent itself, and the token
should say so. Token Exchange's act claim
names the actor operating on the subject's behalf, and it nests. Once there's both a person and an agent,
"who is acting" is a claim you want written down rather than inferred.