Delegation: acting for someone else
Valet keys, garage tickets, and whose authority is this, actually?
Every "Connect your account" button does the same thing. You click Allow, and somebody else's software gets to act for you. Read your calendar, post to your Slack, pull your bank transactions.
That's delegation: your authority, used by someone else.
Most of us click that button without a second thought. So what exactly did you hand over? Whose authority is the app using when it calls an API? And what happens when it needs to pass that authority on to yet another service?
That's this note. I think delegation is the part of authorization that matters most once AI agents show up. There are no agents in this one yet, though. Mostly.
How to read this
People reading this sit anywhere between "what's OAuth?" and "I have written token exchange policies". So most ideas get explained up to three times, from a different angle each time:
- The hotel. One analogy for the whole note. Skip these paragraphs if you know the concept.
- The software. The same idea in one small, made-up and very ordinary company.
- Deeper cut boxes. Details, edge cases and a few opinions for people who do this for a living. Skip them if you are new, nothing later depends on them.
If you'd like the wider basics first (identity, roles, OAuth, tokens, policy engines), How software gets permission today covers them in the same style. Not required, though. Whatever this note needs, it explains.
You know impersonation versus delegation, and you have probably configured token exchange at some point. Skim down to Who does Accounting actually listen to? for the one detail of RFC 8693 I think matters most once agents show up. Then Why this is hard and the wrap-up.
The cast
The hotel. You are staying at a hotel. At check-in, the front desk gives you a key card for room 412. The doors don't know who you are, they just trust the card. You also arrived by car, so the valet has a valet key: it starts the engine and opens the doors, but not the trunk. Later on there's a parking garage down the street, where the valet actually parks the cars, and your friend Alex, who is about to borrow something.
The software. Acme is a company with an internal purchasing app.
- Anna is an engineer at Acme. Her user ID is
usr_8f3a, and order 1041 is the monitor she ordered for her home office. - Acme Purchasing is the app, with an API behind it.
- Acme Accounting is where approved orders end up, and where the invoice PDFs live.
- ExpenseBuddy is a third-party app that builds expense reports. Anna connected it to her Acme account through the usual screen: "ExpenseBuddy wants to read your orders [Allow]".
That's everyone.
As, or for
There are two ways to let someone use your authority. From far away they look the same. Up close they are completely different.
Impersonation: being someone
The hotel. You are at the bar, and Alex offers to grab your jacket from the room. You hand them your key card.
It works. It opens 412. It also opens the pool, the spa and the gym, because it's your card. The door log says you opened 412 at 21:14. As far as the hotel knows, you went upstairs. You didn't. And if Alex decides to have a quick swim on the way back, the pool thinks that was you too.
The software. Impersonation is software acting as a user, with nothing telling the system someone else is involved. If ExpenseBuddy asked for Anna's Acme password and logged in with it (which is how integrations worked before OAuth, and some still do), that's impersonation. So is a support tool's "log in as this customer" button, or an internal service that can mint a session for any user ID it's given.
The API sees Anna. Only Anna. Everything ExpenseBuddy does, Anna did, as far as every log and every policy is concerned.
Impersonation isn't always wrong. A support engineer debugging a customer's broken dashboard
genuinely needs to see exactly what the customer sees. sudo is impersonation, and nobody
wants to get rid of sudo. But the honest versions have one thing in common: they write it
down.
sudo logs the real user next to every command. Kubernetes supports impersonation through
Impersonate-User headers (kubectl --as anna), and its audit log records both the
impersonating user and the impersonated one. AWS AssumeRole sessions carry a session name,
and an optional SourceIdentity that survives role chaining.
The pattern: the system acts as the target, but it knows, and records, who is really behind it. That's impersonation at its best, and it's still worse than delegation, because the policy can't tell the difference. The pool still lets Alex in.
Delegation: acting for someone
The hotel. Different approach. You call the front desk: "My friend Alex is coming to get my jacket, can you give them a card?" The desk makes one. It opens 412 and nothing else. It stops working at 22:00. And the door log says "412 opened by Alex, guest of 412".
Same result, Alex gets the jacket. Completely different system.
The software. That's what OAuth does. When Anna clicked Allow, Acme's login server gave ExpenseBuddy an access token: a small signed document it sends along with every API call. Decoded, it looks like this:
1{2 "iss": "https://login.acme.com",3 "sub": "usr_8f3a",4 "aud": "https://purchasing.acme.com",5 "client_id": "expensebuddy",6 "scope": "orders:read",7 "iat": 1789812000,8 "exp": 17898129009}
The important bit: there are two identities in it. sub is whose authority this is,
Anna's. client_id is who is holding it, ExpenseBuddy. The token says, more or less,
"ExpenseBuddy, acting for Anna, may read orders until 10:39".
The rest is the fine print: iss is who issued it, aud (audience) is the one API that should
accept it, scope is what kind of access it allows, and exp is when it stops working,
fifteen minutes after it was issued (iat).
That's delegation: acting for someone, as yourself, and saying so.
The second identity is what makes it useful. With it, the API can:
- log it honestly: "usr_8f3a via expensebuddy read order 1041"
- treat the delegate differently: Acme might decide that third-party apps never approve orders, even when the user behind them could
- cut the delegate off without touching Anna: revoke ExpenseBuddy, and Anna's own login and every other app keep working
What good delegation looks like
Put the two side by side:
| Lend your card | A card for Alex | ||
|---|---|---|---|
| 1 | Can do less than you | no, everything | yes, just 412 |
| 2 | Expires on its own | only at your checkout | yes, at 22:00 |
| 3 | Revocable without touching you | no, cancel your card | yes |
| 4 | Logs show who actually did it | no, it says you | yes, "Alex, guest of 412" |
| 5 | Doors can treat them differently | no | yes |
That's pretty much the checklist. Good delegation is narrower than the person delegating, expires, can be revoked on its own, is attributable to both parties, and is visible to whoever makes the decision.
Plus one rule that holds it all together: you can't delegate what you don't have. Alex's card can't open the spa if yours can't. ExpenseBuddy can't read her team lead's orders, because Anna can't. The delegate gets the intersection of what the delegator may do and what they chose to hand over.
client_id in a JWT access token comes from RFC 9068, the JWT profile for OAuth access
tokens. OpenID Connect ID tokens have azp, the "authorized party". Microsoft tokens use
azp or appid, depending on the token version. Same idea, three names. When someone says
an API "can't tell whether a request came from the user or from an app", this claim is
usually sitting right there in the token, unread.
Also, consent is a UI feature, not a protocol one. OAuth doesn't say what a consent screen must show. "Read your orders" is whatever the API developer typed into the scope description.
Passing it on
So far, one hop: Anna, ExpenseBuddy, the API. Real systems are rarely that polite.
The hotel. The valet takes your car. But the hotel has no garage of its own, so the valet drives it down the street to a parking garage run by a different company, and hands it over.
What does the garage know? That the hotel valet brought a car. Does it know it's your car? That you only allowed "park it and bring it back", not "take it for a spin"? If the car comes back with 200 extra kilometres on it, whose problem is that?
The software. ExpenseBuddy wants the invoice PDF for Anna's monitor, so it calls
GET /orders/1041/invoice. Purchasing checks the token: Anna's order, scope covers reading
it. Fine. But the PDF lives in Acme Accounting, so Purchasing has to call Accounting.
With what?
Option 1: its own account
Purchasing has a service account, and Accounting trusts it. It can read every invoice, since Purchasing needs them for all kinds of things.
This is the default in most systems I have worked on. It works. And Accounting sees
purchasing-api, not Anna. Anna fell off at the second hop.
That means the only thing standing between ExpenseBuddy and everyone's invoices is Purchasing remembering to check that order 1041 is Anna's. In this endpoint, and in every other one that calls Accounting. Accounting can't help. It never learned there was an Anna.
Purchasing holds broad authority of its own and uses it for whoever asks. Hold that thought, it has a name, and we'll get to it in a minute.
Option 2: forward Anna's token
Purchasing just passes along the token it received.
Look at that token again: "aud": "https://purchasing.acme.com". If Accounting accepts it,
it's accepting a key card that says "for Purchasing only". Skipping the audience
check is a classic bug, except this time it would be on purpose. And now any service that ever receives a token
meant for Purchasing can use it at Accounting too.
Accounting also sees client_id: expensebuddy, so it thinks ExpenseBuddy called it directly.
It has no idea Purchasing was in between.
So, no. Please don't.
Option 3: exchange it
The hotel. The valet doesn't hand the garage your key. They walk up to the garage desk with your valet key and their own hotel badge, and get a garage ticket: "Car of guest 412, dropped off by the hotel valet. Parking only. Pickup by the hotel valet, until 23:00."
The garage never touches your key. The ticket says whose car it is, who brought it and what it's for.
The software. That's token exchange (RFC 8693). Purchasing goes back to Acme's identity provider with two things: Anna's token (the subject token) and proof of its own identity (the actor token). And it asks for a new token, for a different audience:
1POST /oauth/token HTTP/1.12Host: login.acme.com3 4grant_type=urn:ietf:params:oauth:grant-type:token-exchange5&subject_token=<Anna's token, the one ExpenseBuddy sent>6&actor_token=<Purchasing's own workload identity token>7&audience=https://accounting.acme.com8&scope=invoices:read
The identity provider first decides whether this exchange is allowed at all: may Purchasing act for users towards Accounting? If yes, it issues:
1{2 "iss": "https://login.acme.com",3 "sub": "usr_8f3a",4 "aud": "https://accounting.acme.com",5 "scope": "invoices:read",6 "act": {7 "sub": "purchasing-api",8 "act": { "sub": "expensebuddy" }9 },10 "iat": 1789812060,11 "exp": 178981236012}
Accounting now gets a token that is:
- for Accounting (
aud), so it can't be replayed at Purchasing, and tokens meant for Purchasing don't work here - still Anna's authority (
sub), so Accounting can check that invoice 1041 is really hers - narrower:
invoices:read, for five minutes - honest about the chain (
act): Purchasing is acting now, ExpenseBuddy acted before it
The act claim is the "dropped off by the hotel valet" line on the garage ticket. It's how a
token writes delegation down: this is Anna's authority, and this is who is using it.
Who does Accounting actually listen to?
This is the detail I find most interesting, and it's tucked away in section 4.1 of the RFC.
The nested act records the whole chain: Purchasing now, ExpenseBuddy before. But the spec
says that when making an access control decision, the API must only consider the top-level
claims and the current actor. The earlier actors are, in its own words, "informational
only".
So Accounting may decide based on "Anna, via Purchasing". It may log that ExpenseBuddy was involved. It may not decide based on it.
That's a deliberate choice, and for classic microservices it's a sensible one. Every hop checked its own caller, so the last API doesn't have to re-check the whole history. The chain is for the auditors.
But hold on to it. When the chain is Anna, then an assistant, then a sub-agent, then an MCP server, then an API, "every hop checked its own caller" is carrying an awful lot of weight.
The token can carry the whole chain, and the spec tells the API to ignore most of it. That's a reasonable rule for services that do the same thing every time. Whether it still holds when the hops are agents is a question for a later note, on who is actually acting.
- Microsoft's on-behalf-of flow predates RFC 8693, and it's where the phrase "OBO"
comes from. It uses the JWT bearer grant with
requested_token_use=on_behalf_of. The resulting token names the user and the middle-tier app, but not the chain before it. may_actlets a subject token say in advance who is allowed to act for this subject. I have rarely seen it used.- The RFC covers impersonation too. Whether an exchange gives you delegation (with
act) or impersonation (a token indistinguishable from the user's own) is up to the identity provider's policy. Same spec, both answers. - Across trust domains (Acme's identity provider to a supplier's) there is identity chaining, which the IETF has approved and which is waiting to become an RFC as I write this. Inside one domain, Transaction Tokens carry the user's identity and request context through a whole call graph. That one is still a working group draft.
Capabilities: the key is the permission
Everything so far is identity-based. The token names Anna, and the API checks what Anna may do. There is an older, very different way to think about handing authority over.
The hotel. Forget the booking system. The key card itself is the permission. Whoever holds it gets in. The door doesn't care who you are.
That's a feature when you want to hand authority to someone else, and a disaster when it leaks. Let's start with the feature part.
The software. A capability is an unforgeable reference that both names a thing and grants access to it. A Google Docs "anyone with the link can view" URL is a capability. So is a pre-signed S3 URL. So is an open file descriptor on Unix: once a process has one, the file's permissions don't matter anymore.
Delegating a capability is trivial. You hand it over. No identity provider, no consent screen, no exchange.
Why would anyone want that? Because of the confused deputy.
The confused deputy
The confused deputy is from 1988, from a paper by Norm Hardy. I think it's about to become one of the most important ideas in authorization, and the original story is worth telling.
At a timesharing company, a compiler had permission to write any file in its own system directory, because it kept usage statistics there. The company's billing file happened to live in the same directory. Users could also tell the compiler where to write a debug output file. One day a user named the billing file as the output. The compiler checked: can I write there? Yes. And it overwrote the billing records.
The compiler wasn't hacked. It did exactly what it was built to do. It had two sources of authority, its own and the user's, and no way to tell which one it was using for which request.
Hardy's point: if naming a file also meant handing over the authority to write it, the user could only ever have named files they were allowed to write. The name and the authority should travel together.
Now look at Option 1 again. Purchasing, calling Accounting with its own service account, is the compiler. ExpenseBuddy names an order, and Purchasing fetches the invoice with its own authority. The only safeguard is Purchasing remembering to check.
Token exchange is the identity-based fix: Purchasing carries Anna's authority to Accounting, not its own. Capabilities are the other fix. Different routes, same lesson.
A deputy should act with its caller's authority, not its own.
Keys you can shrink
The hotel. Picture a key card you can weaken yourself. Put tape over the pool. Write "until 23:00" on it in ink that doesn't come off. Hand it to Alex. No trip to the front desk. Alex can add more tape before handing it to someone else, but can never peel yours off.
The software. That's attenuation, and it's the best trick capabilities have. Macaroons (a 2014 paper from Google) do exactly this. A macaroon is a token plus a list of caveats, each one a restriction. They are chained with signatures so that anyone holding the macaroon can add a caveat, and nobody can remove one.
1macaroon, issued by Acme to Anna2 caveat: scope = orders:read3 -- Anna adds, before giving it to ExpenseBuddy --4 caveat: order = 10415 caveat: expires < 2026-09-27T10:39Z6 -- ExpenseBuddy adds, before giving it to its PDF worker --7 caveat: action = read_invoice8 signature: 9f2b…
Every holder narrowed it, offline, without asking anyone. When it reaches Purchasing, the API checks the signature and every caveat. The PDF worker can read one invoice, for one order, until 10:39. It can't widen that. Each caveat is signed using the previous signature as the key, so there is no way back up the chain.
"You can't delegate what you don't have" stops being a policy someone has to enforce. It's just maths.
Child ≤ parent, checked offline, no server involved. That's exactly what you'd want when an agent hands part of its job to a sub-agent. It comes back in a later note, together with the one thing it can't do.
Macaroons are verified with a shared secret, so only the issuer can check them. Biscuit does the same thing with public-key signatures, so any service can verify one offline, and it writes caveats in Datalog. Fly.io built its API tokens on macaroons and wrote about it at length.
The downsides are real, though. A capability is a bearer token by definition: leak it, and the finder has it. Revocation needs a server again (a revocation list, or a caveat saying "check with me first"). And "who did this?" is harder to answer when the door never asked. The object-capability crowd has argued for decades that all of this is solvable, and I think they are mostly right. The industry went with identity anyway.
Honestly, OAuth bearer tokens are capabilities with a name written on them. Whoever holds one can use it. The name just lets the API check the rest.
Why this is hard
Before any AI shows up, delegation is already a mess. A few of the usual ways it goes wrong.
Stolen valet keys
The hotel. Someone copies the valet key at the garage. Now they have your car, and every log says "hotel valet".
The software. In practice, a delegated token is a capability. Whoever steals it becomes the delegate.
In August 2025, attackers got hold of OAuth tokens that companies had granted to Salesloft's Drift integration for Salesforce, and used them to export data from hundreds of companies' Salesforce instances. In 2022, OAuth tokens issued to Heroku and Travis CI for GitHub were stolen and used to download private repositories from dozens of organizations, npm among them.
In both cases nobody's password leaked. Every token had been granted properly, by a real admin clicking Allow. The logs said the integration did it. Technically, it did.
Consent is coarse, and forever
The consent screen said "ExpenseBuddy wants to read your orders". Not "the ones from September". Not "for the next hour". Every order, plus a refresh token that keeps working for months, until somebody revokes it.
Quick test: open the list of third-party apps with access to your Google or GitHub account. How many of them do you still use? I'll wait.
Access reviews usually cover roles and groups. Grants to apps mostly fall through the cracks.
Impersonation wearing a delegation badge
Google Workspace domain-wide delegation lets an admin allow a service account to get tokens as any user in the company, by putting that user's email in its token request. It's called delegation, and it's limited per scope. But what comes out is a token for the user, used by a program that could just as easily have picked a different user.
This isn't specific to Google. Any service account that can act as anyone is a hotel front desk clerk who opens any room for whoever asks, except it's a script.
Revocation doesn't travel down the chain
Anna revokes ExpenseBuddy at 10:30. The token Purchasing got from its exchange at 10:28 is a signed JWT, and Accounting will accept it until it expires. Nobody tells Accounting, because nobody knows it has one.
Short lifetimes make that window small. They don't close it. And the longer the chain, the more of these little tokens are floating around, each on its own clock.
The human falls off at hop two
And the most common one, which isn't really a bug anywhere, just the default: option 1. The first service checks the user, and every call after that is service to service, with service accounts. Ask the fourth service in a chain which human a request is for, and the honest answer is "no idea, but the caller seemed confident".
| Failure | Hotel | Acme | Result | |
|---|---|---|---|---|
| 1 | Stolen delegated token | Copied valet key | ExpenseBuddy's refresh token leaks |
|
| 2 | Coarse, permanent consent | Alex's card works all week | orders:read, every order, for months |
|
| 3 | Impersonation as delegation | Clerk opens any room | Service account acting as any user |
|
| 4 | Revocation doesn't cascade | Garage ticket outlives the valet key | Exchanged token outlives the grant |
|
| 5 | Human lost after the first hop | Garage only knows "hotel valet" | Accounting sees purchasing-api |
|
Every one of those should have been a deny. Or at least a question.
One request, end to end
ExpenseBuddy fetches the invoice for Anna's monitor, the good way.
- Delegation. Anna consented earlier. ExpenseBuddy holds a token with
subAnna,client_idexpensebuddy,scopeorders:read,audPurchasing. - First hop. ExpenseBuddy calls
GET /orders/1041/invoice. Purchasing validates the token and checks the intersection: Anna may read order 1041, and the scope allows reading orders. Allow. - Exchange. Purchasing needs the PDF from Accounting. It sends Anna's token and its own
workload identity to the identity provider, asking for
invoices:readat Accounting. - Exchange policy. The identity provider checks that Purchasing may act for users
towards Accounting, and that Anna herself could read invoices. It issues a five-minute
token:
subAnna,actPurchasing, with ExpenseBuddy nested inside. - Second hop. Accounting validates the token (its own audience, not Purchasing's), checks that the invoice for order 1041 is Anna's and that Purchasing is allowed to read invoices for users. Allow.
- The log. Accounting writes
usr_8f3a via purchasing-api (via expensebuddy) read invoice 1041.
What each hop knows:
| Hop | Who it sees acting | Whose authority | Knows about ExpenseBuddy? | |
|---|---|---|---|---|
| 1 | Purchasing API | ExpenseBuddy | Anna's, orders:read | yes, client_id |
| 2 | Identity provider | Purchasing | Anna's, from the subject token | yes |
| 3 | Accounting | Purchasing | Anna's, narrowed to invoices:read | in the log, not in the decision |
| 4 | Accounting, option 1 | Purchasing | Purchasing's own | no, and no Anna either |
The last row is the one most systems actually run.
Wrapping up: permissions, not tasks
So, what does it mean for software to hold authority that started with a human?
Today's answer is actually pretty good. The token names both the human and the software. It's narrower than the human, it expires, and it can be revoked. When it needs to travel, it gets exchanged instead of forwarded, and the chain gets written down. Capabilities offer another route, with attenuation you can do offline. None of this is new, and most of it works.
But look at what was actually delegated.
The consent screen said "ExpenseBuddy wants to read your orders". Anna didn't delegate "build my September expense report". She delegated read orders, and trusted ExpenseBuddy's code with the rest. The token says what ExpenseBuddy may do. What it will do was decided by a developer, reviewed, shipped, and it runs the same way every time.
The purpose never went into the token. It lives in the delegate's code.
Which leaves one question: what happens when whoever holds the valet key decides for themselves where to drive?
The valet key never cared where you go. It didn't need to. Valets drive to the garage, because that's the job, and the job was settled long before anyone handed over a key.
An agent gets the same kind of valet key, except the job is a sentence. "Get me a monitor, keep it under €800." The token can't hold that sentence, and there's no code that holds it either.
What's next
Next note: MCP authorization. MCP is how most agents reach tools today, and its spec had to pick an answer to almost every question in this note: whose token, for which audience, and whether a server may pass it on. (It may not. Option 2 is explicitly forbidden, which I appreciate.)
So what does MCP authorization actually guarantee, and what does it quietly leave to you?
Catch you in the next one.