OAuth learned to describe the transaction. It still cannot prove you meant it.
Rich Authorization Requests gave OAuth the vocabulary to say exactly what a token may do, down to the amount and the destination account. That was a real advance. It did nothing to change how the human's agreement was captured, which is still a click in a browser session.
The most precisely specified fraud you will ever review
Picture the incident review. A payment left a customer's account, and the engineer who pulls the logs finds something almost admirable. The token that authorised it was not some baggy legacy scope. It carried structured authorisation data, and that data was exact: payment initiation, a single transfer, four hundred and eighty euros, one named beneficiary, one IBAN, a ten minute validity window. Every constraint was honoured. The resource server checked each one and found the request entirely within bounds.
The audit trail is immaculate. The consent record shows a consent screen was rendered at 14:02:11 and an approval was received at 14:02:19, eight seconds later, which is fast but not implausible. The token was issued, used once, and expired.
The customer, when contacted, says they have never seen that beneficiary and did not make a payment that afternoon.
Nothing in the system is broken. No control failed. The specification was implemented correctly, the authorisation server behaved as designed, and the token described the transaction with more precision than any previous generation of OAuth could manage. The precision is exactly what makes the incident so uncomfortable to read, because the natural instinct on seeing that level of detail is to assume somebody must have looked at it and agreed.
Nobody had. That assumption is the subject of this post.
Does an OAuth scope prove the user consented? No. A scope, including a rich structured scope under RFC 9396, states what a token is permitted to do. It carries no evidence that a specific human saw those details and agreed to them. Proof of that requires a signature over the authorisation details themselves, produced on a device the requesting software does not control, and carried alongside the token.
What problem do Rich Authorization Requests actually solve?
To see why RFC 9396 matters, look at what came before it.
Classic OAuth expresses authority as scopes, which are space delimited strings. A client asks for payments or accounts:read, the user approves, and the token carries the string. This works well for the case it was designed for, which is coarse grained delegated access to an API. It works poorly the moment authority needs parameters.
Consider a hall pass. A pass that says "library" is a scope. It tells the person at the door which category of thing you may do. It says nothing about which section, for how long, or to collect what. For most of a school day, "library" is precise enough. For a payment, it is nowhere near.
You cannot express "transfer up to five hundred euros to this specific account within the next ten minutes" as a string in a list without inventing a private encoding, and people did invent them, which is how the industry acquired scopes like payment_initiation_EUR_500_ACME and a great deal of regret. Open banking made this untenable, because a payment initiation service provider genuinely needs the authorisation to name the amount and the payee, and the bank genuinely needs to enforce those values rather than a category.
RFC 9396 fixes this properly. It introduces an authorization_details parameter carrying a JSON array of typed objects, each with a type field naming a schema the authorisation server understands, and whatever structured fields that schema defines. The authorisation server presents those details to the user, enforces them, and reflects them in the token response so the resource server can see what was actually granted.
"authorization_details": [
{
"type": "payment_initiation",
"actions": ["initiate"],
"instructedAmount": { "currency": "EUR", "amount": "480.00" },
"creditorName": "Aurora Components BV",
"creditorAccount": { "iban": "NL02ABNA0123456789" },
"remittanceInformationUnstructured": "Invoice 20261-114"
}
]
That is a much better hall pass. The section, the duration and the specific book are all named, the door staff can enforce them, and the whole thing is typed rather than a string somebody parsed with a regular expression. The working group did a genuinely good piece of design here, and the related profiles built on it, including the Financial grade API work at the OpenID Foundation, are serious engineering aimed at the highest risk flows in the industry.
Now look at the bottom of the pass, where the signature goes.
Why is a precise scope not the same as consent?
Here is the distinction the whole post rests on, and it is worth slowing down for.
RFC 9396 governs expression. It defines a vocabulary for describing authority precisely and a protocol for carrying that description through the authorisation flow into the token. It is deliberately, correctly silent on how the human agreed, because that is a different layer and a specification that tried to mandate it would be overreaching.
So the question "how did the user approve these details" is answered, in every deployment, by whatever the authorisation server happens to do. And what the authorisation server happens to do, almost universally, is render a web page inside a browser session and treat a form submission as approval.
That means the evidence of consent for a precisely specified payment is a click in a session. And a click in a session is exactly the artifact that every attack described elsewhere in this series produces without any human forming an intention.
Three ways a perfectly scoped token gets issued with no human behind it
The session was stolen. An adversary in the middle phishing kit relays the genuine authorisation server, the victim authenticates for real, and the attacker holds the resulting session. Any consent screen rendered into that session can be approved by the attacker, and it will be approved by an authenticated, entitled, correctly identified principal. This is the mechanism set out in your MFA worked perfectly, the attacker was already inside the session.
The consent was collected without a meaningful display. In the OAuth device authorization grant, described in RFC 8628, the user enters a code on a legitimate page and approves an application whose name the attacker chose. Microsoft attributed a February 2025 campaign using this technique to the actor it tracks as Storm-2372, and research notes published by the Cloud Security Alliance during 2026 described a substantial surge in such campaigns affecting hundreds of Microsoft 365 organisations. Microsoft reported in April 2026 on campaigns using automation to generate live codes on demand. Treat the specific counts in this reporting as directional, since each figure reflects what one vendor could see. The full mechanism is covered in the phishing page that is not a phishing page.
The clicker was software. An agent operating a browser on the user's behalf will encounter consent screens and will resolve them, because resolving obstacles is what it was built to do. It is not attacking anything. It is completing the task. The general form of this problem is set out in approval theater: any approval control an agent can reach, an agent can satisfy.
In all three cases the resulting token is flawless. It is correctly scoped, correctly bound, correctly logged, and it describes a transaction that no human ever agreed to. The precision of the description is not evidence about the human, and a downstream resource server that reads detailed authorisation data as though it were a signed instruction is making an inference the protocol never licensed.
Do DPoP, PKCE and token binding close the gap?
They are all valuable and none of them addresses this. It is worth being exact about what each one does, because they are frequently offered as though they did.
| Mechanism | What it establishes | What it does not establish |
|---|---|---|
| Scopes (classic OAuth) | A category of permitted operation | Parameters, and anything about the human |
| RFC 9396 Rich Authorization Requests | Precisely what the token may do, in typed structured form | That a human saw or agreed to those details |
| PKCE (RFC 7636) | That the party redeeming the code is the party that started the flow | Anything about consent, which happens before the exchange |
| DPoP (RFC 9449) | That the token is held by the client key that requested it, so a stolen token is not replayable elsewhere | That the client is acting on a human instruction |
| Short token lifetimes | A narrow window of exposure | Anything at all, if the token is used inside the window, which it is |
| Grant management | Lifecycle visibility and revocation of a grant | Presence at the moment the grant was created |
| Step up authentication at the authorisation server | That the session was recently reauthenticated | That the reauthenticated human approved these specific details, rather than merely logging in again |
| Signature over authorisation details | That a specific enrolled human approved this exact object at this time | That the human read it, understood it, or was not coerced |
DPoP deserves particular credit and particular clarity. Sender constrained tokens are a genuine improvement over bearer tokens, and they solve token theft, which was a serious problem. But binding a token to a client key answers "is the same software presenting this token" and not "did a person ask for it". A malicious client with its own key produces a perfectly DPoP bound token. An agent produces one too, honestly and correctly, on behalf of a user who never saw the details.
This is the same shape as the observation in passkeys prove you logged in. A whole category of excellent cryptography secures the channel and the principal, and then the question of what the human meant is answered by a page.
Why does European regulation get this right in exactly one place?
There is one domain where the missing property is not optional, and it is instructive that it took a regulator to impose it.
Under the European regulatory technical standards on strong customer authentication, a payment authentication has to be dynamically linked to the transaction. The authentication code must be specific to the amount and the payee, a change to either must invalidate the code, and the payer must be made aware of both. That requirement, in substance, is a signature over the transaction details rather than over the session.
So the industry already knows how to do this. It knows the property is achievable, it knows how to specify it, and it has deployed it at national scale across an entire continent's retail payments. It has simply not generalised it, because outside the regulated payment flow nobody is compelled to.
This is the second time the same pattern appears in this series. The identical observation applies to card authentication, where the risk based flow makes a judgment from device data and the challenge flow authenticates a session, while dynamic linking, in the one jurisdiction that mandates it, binds the approval to the purchase. A property that is understood, specified, and shipped in one domain is absent everywhere the mandate does not reach. That recurrence is the strongest available evidence that this is a governance gap rather than a technical one.
What would signed consent look like in OAuth terms?
The composition is small, and it deliberately changes nothing about RFC 9396 itself.
Step one. At the consent step, the authorisation server canonicalises the authorization_details array it is about to grant, and derives a challenge from its hash.
canonical = jcs_canonicalize(authorization_details) # RFC 8785 style
challenge = sha256(canonical) # 32 bytes
Step two. Rather than accepting a form submission as approval, the authorisation server requests a signature over that challenge from the user's enrolled device, and renders the details on that device so the display and the approval share a trust domain. The user sees the amount and the beneficiary on the phone in their hand, not only in the browser tab that an attacker or an agent may control.
Step three. The resulting receipt is carried as a claim in the issued token, or delivered alongside it.
{
"iss": "https://as.bank.example",
"sub": "cust_8841",
"authorization_details": [ { "type": "payment_initiation", "...": "..." } ],
"cnf": { "jkt": "0ZcOCORZ..." }, // DPoP binding, unchanged
"human_consent": {
"receipt": "eyJhbGciOiJFZERTQSIs...", // Ed25519 over the details hash
"details_hash": "sha256:9f2c...4b10",
"signed_at": "2026-09-25T14:02:19Z",
"verify": "https://manav.id/.well-known/manav-keys"
}
}
Step four. The resource server, which already validates the token and enforces the authorisation details, adds one check that requires no network call.
def authorize(token, request):
details = token.authorization_details
if not matches(request, details):
return Deny("request outside granted authority")
hc = token.get("human_consent")
if hc is None:
return Deny("no human consent receipt")
# recompute the hash of what was granted, offline
if hc["details_hash"] != sha256(jcs_canonicalize(details)):
return Deny("receipt does not cover these details")
# verify Ed25519 against the published key, no callback
if not ed25519_verify(hc["receipt"], PUBLISHED_KEY):
return Deny("receipt signature invalid")
if age(hc["signed_at"]) > MAX_CONSENT_AGE[details[0]["type"]]:
return Deny("consent too old for this operation")
return Allow()
The important property is in the comments. The resource server does not call the authorisation server, does not call Manav, and does not trust either of them at the moment of the decision. It recomputes a hash and verifies a signature against a published key. A consent receipt from six months ago verifies exactly as well as one from six seconds ago, which is what makes it usable as dispute evidence long after the fact.
How does this apply to agents?
This is where the gap stops being theoretical, because agent platforms are building on OAuth right now.
The Model Context Protocol authorization specification uses OAuth for a client to obtain access to a server's tools. It handles client authentication properly and, quite reasonably, leaves human authorisation of individual tool invocations to the implementer. That is a defensible separation of concerns and it has a predictable consequence: every server author decides independently whether an operation that moves money needs a human, and most decide by not deciding. The practical remedy at that layer is covered in your MCP server can move money.
At the OAuth layer, the structure that fits is a chain rather than a single consent. The human signs a delegation once, defining scope, constraints and expiry. The agent then obtains rich authorisation tokens under that delegation, and each token carries a reference back to the human signed root rather than to a consent screen the agent clicked. A verifier walks from the token to the delegation to the human key, offline, checking that the token's authorisation details fall inside the delegated scope at every hop. The mechanics of that walk, including depth limits and monotonic narrowing, are in can an agent delegate to another agent.
The distinction matters commercially as well as technically. A scoped token issued to an agent under a human signed delegation is defensible in a dispute. A scoped token issued to an agent because the agent clicked approve is a precisely worded description of an event nobody authorised.
Honest limits
- A signature does not mean the human read it. Someone can sign a payment they did not understand, or sign under coercion, or sign while a persuasive stranger stays on the phone. This binds intent to an action. It does not audit judgment, and no cryptography does.
- Consent fatigue is a real risk and a real cost. If every authorisation detail requires a signature, users will approve reflexively and the control becomes noise. The design only works if the set of gated operation types is small and chosen by value at risk, which is a policy decision no specification can make for you.
- This requires authorisation server support. A resource server cannot demand a consent receipt that no authorisation server issues. Adoption starts at the authorisation server, which is a smaller population but not one a vendor can change unilaterally.
- Manav has not shipped an authorisation server plugin. The signing and offline verification primitives are shipped. A packaged integration for Keycloak, Auth0, Okta or ORY is not, and the receipt claim described here is a proposal rather than a ratified standard. Manav has filed drafts on receipts and delegation chains at the IETF and the OpenID Foundation; filed is not adopted, and it would be better for everyone if a working group specified the claim format rather than any single vendor.
- It does not fix a compromised device. If the phone that displays and signs is itself controlled by the attacker, the display and the signature both belong to the attacker. Separating the display domain from the browser raises the cost considerably. It does not create an incorruptible one.
What to do this week
- Inventory your authorisation detail types, or your equivalent high value scopes. Most organisations have between three and ten that genuinely matter, and a long tail that does not.
- For each, write down what evidence you currently hold that a human approved it. If the honest answer is "a row in the consent table written by our own authorisation server", write that down, because that is the finding.
- Check whether your consent screens are reachable inside a session an adversary in the middle kit could hold. If your authorisation server accepts a form post as approval, they are.
- Disable the device authorization grant on tenants that do not need it. It is a free configuration change and it removes an entire attack class before you buy anything.
- Measure the time between consent and token use for your riskiest flow. Long gaps and instant gaps are both interesting, for different reasons.
- Ask your authorisation server vendor whether they can bind approval to a device held assertion over the authorisation details. The answer today is usually no, and asking is how that changes.
- If you are building agent tooling on OAuth, decide now which tool invocations require a human root, before the scope surface grows past the point where anyone can enumerate it.
A working signing flow takes about a minute to try in the signing demo, and the receipt structure and offline verification path are documented in the developer documentation.
Frequently asked questions
Does an OAuth scope prove the user consented? No. A scope states what a token may do. Even a rich structured scope under RFC 9396 describes permitted authority, not the circumstances under which it was granted. Proof that a specific human approved those details requires a signature over the details themselves, carried with the token and verifiable independently of the authorisation server that issued it.
What is RFC 9396? Rich Authorization Requests, an OAuth extension that replaces coarse scope strings with an authorization_details parameter carrying typed JSON objects. It lets a client request, and an authorisation server enforce, authority with parameters such as an amount, a beneficiary account and a validity window. It was driven largely by open banking requirements that plain scopes could not express.
Does DPoP solve OAuth consent phishing? No. DPoP, defined in RFC 9449, binds a token to the client's key so a stolen token cannot be replayed by a different client. That is valuable against token theft. It says nothing about whether a human asked the client to obtain the token, so a consent phishing flow produces a perfectly DPoP bound token.
How do you stop OAuth consent phishing? Reduce the surface first, by disabling unused grant types such as the device authorization grant and restricting which applications may request consent. Then change what counts as approval: require an assertion from an enrolled device over the authorisation details, rendered on that device, so an approval collected inside a stolen or automated browser session is not sufficient.
How does MCP handle human authorization? The Model Context Protocol authorization specification builds on OAuth to authenticate clients to servers and leaves human approval of individual tool calls to the implementer. That is a reasonable scope boundary for a specification, and it means human authorisation is a deployment decision. Server authors handling consequential operations have to add it themselves.
Is dynamic linking the same as signed consent? Substantially, yes, within its domain. The European strong customer authentication rules require the authentication code to be specific to the amount and payee, which is a signature over transaction details rather than over a session. The limitation is scope: it applies to regulated payment flows, and the same property is absent from general OAuth authorisation everywhere it is not mandated.
Can a resource server verify consent without calling the authorisation server? Yes, if the consent receipt is a signature over a hash of the authorisation details, verifiable against a published key. The resource server recomputes the hash from the details it already holds and checks the signature locally. That keeps the check available during an outage and keeps the evidence valid long after the token has expired.
Sources
- IETF RFC 9396, OAuth 2.0 Rich Authorization Requests. rfc-editor.org/rfc/rfc9396.html
- IETF RFC 9449, OAuth 2.0 Demonstrating Proof of Possession (DPoP). rfc-editor.org/rfc/rfc9449.html
- IETF RFC 7636, Proof Key for Code Exchange. rfc-editor.org/rfc/rfc7636.html
- IETF RFC 8628, OAuth 2.0 Device Authorization Grant, including its security considerations. rfc-editor.org/rfc/rfc8628.html
- IETF RFC 8785, JSON Canonicalization Scheme. rfc-editor.org/rfc/rfc8785.html
- OpenID Foundation, Financial grade API working group and profiles. openid.net/wg/fapi
- Model Context Protocol, authorization specification. modelcontextprotocol.io
- Microsoft Security Blog, reporting on device code phishing including the February 2025 campaign attributed to Storm-2372 and April 2026 reporting on automated code generation. microsoft.com/en-us/security/blog
- Cloud Security Alliance, research notes on OAuth device code phishing published during 2026. cloudsecurityalliance.org
- European Banking Authority, regulatory technical standards on strong customer authentication and common and secure communication, including dynamic linking requirements. eba.europa.eu
A precisely scoped token that no human approved is not a small breach. It is a precisely scoped one.