You kept the token. You did not keep the consent.
When a user connects their mailbox to your agent, two things are created: an access token, which every system stores, and a consent — a claim by a specific person, at a specific time, that a specific scope may be used for a specific purpose — which almost nobody stores. The token answers "can we call this API". Every question you will actually be asked, by a user or an auditor or a customer's security team, is answered by the record you threw away.
Delegation creates a record, and the token is not it.
An agent acting on a user's behalf is exercising delegated authority. That is a good design — far better than one service account holding the union of everyone's access, as agent identity & permissions argues. But delegation is a relationship with terms, and the credential is only its side effect.
The terms are five facts: who granted it, what scope they granted, when, on whose behalf — themselves, or an organisation they administer — and for what purpose they were told it would be used. A refresh token encodes at most the second. Ask a typical agent deployment which user consented to which scope on which date and it will answer by inspecting a credential store, which is to say by inferring the past from an object that gets silently overwritten every time it refreshes.
The practical consequence arrives the first time a customer asks what your agent has been allowed to do in their tenant, and the honest answer is a list of currently-valid tokens rather than a history of decisions.
Four questions, and the fields that answer them.
Design the record backwards from what gets asked. These four are asked routinely, and each maps to something you either wrote down at grant time or cannot reconstruct.
- "Who allowed this?" — from a customer whose data moved. Needs the grantor's identity, whether they acted for themselves or as an administrator for others, and the authority they held at the time, not today.
- "Under what permission was this action taken?" — after any disputed write. Needs a consent identifier stamped on the action record, which means the audit trail has to carry it forward from grant to call.
- "What were they told it was for?" — from a regulator, a privacy review, or a user who feels surprised. Needs the purpose text as displayed, versioned. The scope string is not the purpose; nobody consents to
https://www.googleapis.com/auth/gmail.modify, they consent to a sentence you wrote next to it. - "Is it still allowed?" — the question that looks trivial and is not. Needs the current state and the transitions: granted, expanded, expired, revoked by user, revoked by admin, revoked by you.
Note that three of the four are questions about the past, and the past is exactly what a credential store does not keep. A consent ledger is append-only for the same reason a general ledger is: the current balance is derivable, and the history is not.
Granted scope, exercised scope, and understood purpose.
Three different things get conflated, and complaints live in the gaps between them.
Granted scope is what the OAuth provider issued. Exercised scope is the set of operations your agent actually performs — usually a small subset, because providers offer coarse scopes and you took the one that covered your case. Understood purpose is what the user believed they were agreeing to, which came from your sentence and not from the provider's dialog.
Two disciplines close the gaps. First, record the exercised scope, not just the granted one: a monthly report showing that an agent granted full mailbox modify access has only ever read and labelled is the strongest possible answer to a security review, and it costs one aggregation over data you already have. Second, treat purpose as versioned text with the grant, so that when the product does more next quarter, you can tell the difference between users who consented to the old sentence and users who consented to the new one.
Then act on the first discipline: if exercised scope has been a strict subset for a quarter, request the narrower scope. This is the concrete, unglamorous form of least privilege that scoped credentials keeps asking for, and the usage data is what makes it an easy decision rather than a guess.
Revocation is the path nobody tests.
A user opens their Google or Microsoft account settings and disconnects your app. What happens next in your system is usually undesigned, and it is the single most common gap in this area.
The token dies, and you find out when a call returns 401 — possibly hours later, possibly during an unattended run. That is the easy part. The hard part is that revocation is about authority, and authority has already propagated:
- Derived data outlives the credential. Everything the agent read is now in a transcript, a summary, an extracted memory, a vector index, and a cache. None of that is reachable by the provider's revoke button, and a user who revokes reasonably believes it is.
- Queued and scheduled work still holds intent. A background run enqueued before revocation will execute after it unless something checks. Every job that acts on a user's behalf must re-validate consent at execution time, not at enqueue time.
- Multi-user surfaces get quietly asymmetric. In a shared workspace, one member's revocation changes what the agent may do for everyone, and nothing in the UI says so.
- Downstream effects do not unwind. The tickets it filed and the messages it sent remain. Revocation stops future authority; it does not reverse past action, and telling the user otherwise is the mistake.
So write the fan-out explicitly: on revocation, stop future calls, cancel or fail queued work for that grant, decide and document what happens to derived data, and record the revocation as an event with a timestamp. Then test it like a restore — a revocation path that has never been exercised is in the same condition as a backup that has never been restored, and both are discovered at the worst moment.
Re-consent is a real event with three common triggers.
Consent is not permanent, and the moments it has to be renewed are predictable enough to plan for.
- Scope expansion. The agent gains a capability the original grant did not cover. This needs a new grant, not a silent widening — and the ledger should show both, so "when did we start being able to send mail" has an answer.
- Purpose change. The scope is unchanged and the use is new: data read for support triage now trains a suggestion feature. No OAuth flow is technically required, which is exactly why this one gets skipped, and it is the one a privacy regulator cares most about.
- Changing the OAuth client. Tokens are issued against a specific client, so switching integration vendors — or moving from a platform's shared client to your own — invalidates every existing grant and forces every user through consent again. This is the migration cost teams discover late; see agent connector platforms for why registering your own client at adoption is a day of work that prevents a quarter of one.
Administrator consent deserves its own record shape. When an IT admin authorises your app for a whole tenant, the grantor and the subjects are different people, the scope covers users who never saw a dialog, and the withdrawal path runs through the admin rather than the individual. Storing that as if it were ordinary user consent is how an agent ends up acting for someone who never agreed to anything.
Make it one queryable ledger, and retain it against the action.
The implementation is smaller than the discussion. One append-only table, one identifier, one rule about retention.
- One consent ledger, whoever holds the tokens. If a connector platform runs your OAuth, its dashboard is its record, not yours, and it will not survive a vendor change. Keep your own row on every grant event: grantor, subject, provider, scope, purpose version, client ID, timestamp, and outcome.
- Stamp the consent ID on every action. One field on the trace, next to the tool-call record. It is what turns "under what permission" from a reconstruction into a join — and the same discipline as the decision receipts you keep for the agent's own choices.
- Retain the consent as long as the actions it authorised. Deleting a grant record when the grant expires leaves you with actions whose authority cannot be explained. The consent record is part of the action's evidence, so it inherits the action's retention and any legal hold.
- Show it back to the user. A screen listing what is connected, what it has actually done, and a working disconnect is both good product and the cheapest way to discover that your revocation path is broken.
Start with two fields and one test. Write a consent row at every grant — grantor, subject, scope, purpose version, client ID, timestamp — and stamp its ID on every action the agent takes under it; that pair answers three of the four questions in STEP 2 and costs an afternoon. Then run one revocation end to end on a real account, in staging, and watch what your system does with the queued job and the cached data: that rehearsal is where teams discover the gap, and it is far better discovered by you than by the user who pressed the button.
Related: audit trails for the record this hangs off, data governance for what happens to what the agent read, accountability & roles for who answers when the grantor and the operator differ, and the EU AI Act for agents for the disclosure obligations that sit alongside this one.