deask.
note2026-10-23

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.

note
If you do IAM for a living

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.

note
Deeper cut: impersonation with a diary

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:

json
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
Impersonation versus delegationTwo panels. Impersonation, acting as Anna: in the hotel, Alex uses your own key card, which opens 412, the pool, the spa and the gym, and the door log says 412 was opened by you. In software, the token has sub usr_8f3a and scope orders:read, and nothing says who is holding it; the API log reads "usr_8f3a read order 1041". Delegation, acting for Anna: the front desk makes a card for Alex that opens 412 only, until 22:00, and the door log says "412 opened by Alex, guest of 412". The token has sub usr_8f3a, client_id expensebuddy (highlighted) and scope orders:read, and the API log reads "usr_8f3a via expensebuddy read order 1041".IMPERSONATION · AS ANNATHE HOTEL Your own key cardopens 412, the pool, the spa, the gymdoor log 21:14 412 opened by youTHE SOFTWAREACCESS TOKENsub: usr_8f3anothing says who is holding itscope: orders:readapi log usr_8f3a read order 1041Whoever holds it is Anna.DELEGATION · FOR ANNATHE HOTEL A card for Alexopens 412 only, until 22:00door log 21:14 412 opened by Alex, guest of 412THE SOFTWAREACCESS TOKENsub: usr_8f3aclient_id: expensebuddyscope: orders:readapi log usr_8f3a via expensebuddy read order 1041Two names: whose authority, and who holds it.
fig 1Acting as someone versus acting for them. With impersonation, every log and every policy only ever sees Anna. With delegation, the token also names who is holding her authority, so the API can log it, limit it and revoke it separately.Lending Alex your key card, or asking the front desk for a card in Alex’s name.

What good delegation looks like

Put the two side by side:

Lend your cardA card for Alex
1Can do less than youno, everythingyes, just 412
2Expires on its ownonly at your checkoutyes, at 22:00
3Revocable without touching youno, cancel your cardyes
4Logs show who actually did itno, it says youyes, "Alex, guest of 412"
5Doors can treat them differentlynoyes

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.

note
Deeper cut: "who is holding it" has many names

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:

http
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:

json
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.

Three ways to make the second hopThree columns, each ExpenseBuddy to Purchasing to Accounting, with Anna’s token on the first hop. Option 1, own account: Purchasing calls Accounting with its own token, so Accounting sees purchasing-api acting for nobody, with its own rights to every invoice. Anna fell off at the second hop. Option 2, forward it: Purchasing passes on Anna’s token, whose audience is Purchasing, so Accounting sees expensebuddy apparently calling it directly, for usr_8f3a, with the wrong audience. Option 3, exchange it: Purchasing gets a new token for Accounting, so Accounting sees purchasing-api acting for usr_8f3a, with expensebuddy before it. Anna is on every hop.OPTION 1 · OWN ACCOUNTAnna’s tokenpurchasing-api’s own token ExpenseBuddy Purchasing AccountingACCOUNTING SEESacting: purchasing-apifor: nobodyrights: every invoiceAnna fell off at the second hop.OPTION 2 · FORWARD ITAnna’s tokenAnna’s token, aud = purchasing ExpenseBuddy Purchasing AccountingACCOUNTING SEESacting: expensebuddy?for: usr_8f3aaud: purchasingA token meant for someone else.OPTION 3 · EXCHANGE ITAnna’s tokennew token for Accounting ExpenseBuddy Purchasing AccountingACCOUNTING SEESacting: purchasing-apifor: usr_8f3abefore: expensebuddyAnna is on every hop.
fig 2Three ways for Purchasing to call Accounting while serving Anna’s request. The first hop is the same in all three. Only the exchanged token gets Anna, and the chain behind her, to the second hop.The garage gets your car from the valet. Only a garage ticket says whose car it is.
Token exchange between Purchasing, Acme login and AccountingPurchasing sends Acme login two tokens: Anna’s token as the subject and its own workload identity as the actor, asking for invoices:read at Accounting. Acme login checks that Purchasing may act for users towards Accounting, and that Anna herself may read invoices. It returns a new token with sub Anna and act purchasing-api, valid for five minutes. Purchasing calls Accounting with it. Accounting checks that the audience is Accounting, that invoice 1041 is Anna’s and that Purchasing may read invoices for users, and returns the invoice PDF. Purchasingactor Acme loginidentity provider Accountingresource serverAnna’s token + own tokenfor accounting · invoices:readEXCHANGE POLICY purchasing may act for users Anna may read invoicesnew token · 5 minsub Anna · act purchasing-apiGET invoice 1041 + new tokenACCOUNTING CHECKS aud is accounting invoice 1041 is Anna’s purchasing may readinvoice.pdf
fig 3Token exchange (RFC 8693). Purchasing trades Anna’s token and its own identity for a new token meant only for Accounting: still Anna’s authority, narrower, five minutes, and with Purchasing named as the one acting.Your valet key and the valet’s badge go in at the garage desk. A garage ticket comes out.

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.

idea
Remember this one

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.

note
Deeper cut: token exchange in the wild
  • 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_act lets 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.

The confused deputy, in 1988 and at AcmeTwo panels with the same shape. 1988: a user tells the compiler to write its debug output to (SYSX)BILL, the billing file, which lives in the compiler’s own directory. The compiler writes there using its own permission to write to that directory. The user couldn’t write there, the compiler could, so it did. Acme, option 1: ExpenseBuddy asks Purchasing for the invoice for order 1041. Purchasing fetches it from Accounting using purchasing-api’s own account, which may read every invoice. ExpenseBuddy can’t read other people’s invoices, Purchasing can, so it might.1988 · THE COMPILERdebug output: (SYSX)BILLthe compiler’s own permission A usernames the debug output file The compilermay write to its own directory (SYSX)BILLthe billing fileThe user couldn’t write there.The compiler could, so it did.ACME · OPTION 1invoice for order 1041purchasing-api’s own account ExpenseBuddyasks for an invoice Purchasingmay read every invoice Accountingtrusts purchasing-apiExpenseBuddy can’t read other people’s invoices.Purchasing can, so it might.
fig 4The same shape, 38 years apart. A caller with little authority names a target, and the deputy acts on it with its own, much bigger authority. The highlighted arrow is where the caller’s request quietly becomes the deputy’s permission.A front desk clerk opening a room for whoever asks confidently enough.

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.

text
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.

Attenuating a macaroon from Anna to ExpenseBuddy to its PDF workerAcme issues Anna a macaroon with the caveat scope = orders:read. Anna adds order = 1041 and expires < 10:39 before giving it to ExpenseBuddy. ExpenseBuddy adds action = read_invoice before giving it to its PDF worker. Each step changes the signature. The Purchasing API checks the signature chain and all four caveats and allows the request. Below, the same thing as nested boxes: Anna’s authority contains ExpenseBuddy’s, which contains the PDF worker’s. Child is less than or equal to parent, checked offline, with no server involved.1 · ISSUED BY ACME2 · ANNA ADDS3 · EXPENSEBUDDY ADDS4 · THE API CHECKS Annascope = orders:readsig 3a71… ExpenseBuddyscope = orders:readorder = 1041expires < 10:39sig c04e… PDF workerscope = orders:readorder = 1041expires < 10:39action = read_invoicesig 9f2b… Purchasing API signature chain orders:read order 1041 10:31 < 10:39 read_invoice ALLOWANNA · EVERY ORDER SHE CAN READEXPENSEBUDDY · + ORDER 1041, UNTIL 10:39PDF WORKER · + READ_INVOICEchild ≤ parent, checked offline, no server involved
fig 5Attenuation with a macaroon. Each holder adds caveats before passing it on (new ones highlighted), nobody can remove one, and each caveat changes the signature. At the end, the API checks the signature chain and every caveat.A key card you can put more tape on, but never peel the tape off.
idea
Remember attenuation

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.

note
Deeper cut: why capabilities didn't win (yet)

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.

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".

FailureHotelAcmeResult
1Stolen delegated tokenCopied valet keyExpenseBuddy's refresh token leaks allow
2Coarse, permanent consentAlex's card works all weekorders:read, every order, for months allow
3Impersonation as delegationClerk opens any roomService account acting as any user allow
4Revocation doesn't cascadeGarage ticket outlives the valet keyExchanged token outlives the grant allow
5Human lost after the first hopGarage only knows "hotel valet"Accounting sees purchasing-api allow

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.

  1. Delegation. Anna consented earlier. ExpenseBuddy holds a token with sub Anna, client_id expensebuddy, scope orders:read, aud Purchasing.
  2. 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.
  3. Exchange. Purchasing needs the PDF from Accounting. It sends Anna's token and its own workload identity to the identity provider, asking for invoices:read at Accounting.
  4. 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: sub Anna, act Purchasing, with ExpenseBuddy nested inside.
  5. 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.
  6. The log. Accounting writes usr_8f3a via purchasing-api (via expensebuddy) read invoice 1041.
One request, end to end: ExpenseBuddy fetches the invoice for Anna’s monitorDelegation: Anna allows ExpenseBuddy to read her orders, and Acme login gives ExpenseBuddy a token with sub Anna and client expensebuddy. First hop: ExpenseBuddy asks Purchasing for the invoice of order 1041. Purchasing checks that order 1041 is Anna’s and that the scope is orders:read. Exchange: Purchasing sends Anna’s token and its own identity to Acme login. Exchange policy: Acme login checks that Purchasing may act for users and that Anna may read invoices, then returns a new five-minute token with sub Anna and act purchasing-api. Second hop: Purchasing calls Accounting with the new token. Accounting checks the audience, that invoice 1041 is Anna’s and that Purchasing may read invoices, and returns the PDF, which Purchasing passes back to ExpenseBuddy. The log: Accounting writes usr_8f3a via purchasing-api (via expensebuddy) read invoice 1041. Annauser ExpenseBuddyclient Purchasingfirst API Acme loginidentity provider Accountingsecond APIallow ExpenseBuddy: orders:read DELEGATIONthe valet keyaccess tokensub Anna · client expensebuddyGET invoice 1041as Anna, via ExpenseBuddy FIRST HOP 1041 is Anna’s orders:readexchangeAnna’s token + own identity EXCHANGEthe garage desk may act for users Anna reads invoices EXCHANGE POLICYnew token · 5 minsub Anna · act purchasingGET invoice 1041 + new tokenas Anna, via Purchasing, via ExpenseBuddy SECOND HOPthe garage ticket aud is me 1041 is Anna’s actor may readinvoice.pdfinvoice.pdfaccounting log usr_8f3a via purchasing-api (via expensebuddy) read invoice 1041 THE LOG
fig 6The walkthrough from the text. Follow the highlighted token: Anna’s authority reaches Accounting a bit narrower at every hop, each service checks its own caller, and the log at the end still names everyone involved.

What each hop knows:

HopWho it sees actingWhose authorityKnows about ExpenseBuddy?
1Purchasing APIExpenseBuddyAnna's, orders:readyes, client_id
2Identity providerPurchasingAnna's, from the subject tokenyes
3AccountingPurchasingAnna's, narrowed to invoices:readin the log, not in the decision
4Accounting, option 1PurchasingPurchasing's ownno, 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.

allowed
Delegation today hands over permissions, not tasks. It works because the delegate's code supplies the task.

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.