Bearer Tokens Weren't Built for Agents: DPoP and the Sender-Constrained Login Handshake
9 min read Fullmakt Team
- authentication
- credentials
- oauth
- agents
- traceability
- governance
Every OAuth access token in wide use today answers one question: is this a valid token? It does not answer a second, more important one: is the caller presenting it the same party the token was issued to? A bearer token, true to its name, is valid for whoever bears it. Copy it, log it, paste it into the wrong Slack channel, and it works exactly as well for the thief as it did for the agent it was minted for. Nothing in the token itself notices the difference.
That gap has always existed. It mattered less when the typical holder of an access token was a browser tab, behind TLS, on a device a human was sitting in front of. It matters enormously more once the typical holder is an unattended AI agent — a process with no human watching its outputs in real time, frequently handling untrusted content, frequently the exact kind of principal a lethal-trifecta scenario is built to trick into leaking whatever’s in its own context. DPoP — Demonstrating Proof-of-Possession, RFC 9449 — closes exactly this gap, and it does it at the one layer most agent authentication schemes still leave open: the handshake itself.
What a bearer token actually proves
A bearer access token proves one fact: someone, at some point, completed a login flow that resulted in this string being issued. It proves nothing about who is using it now. The authorization server checked identity once, at mint time, and from that moment forward the token is a bearer instrument — a signed claim with no cryptographic tie back to the party it was handed to.
This is why every agent credential leak looks the same regardless of cause. Whether the token was exfiltrated via a poisoned tool response, echoed into a log a second system reads, or copied by a second process sharing the same fanned-out subagent run, the result is identical: a perfectly valid token, now usable by whoever has it, doing whatever its scope allows, indistinguishable in an access log from the agent it was issued to. The audit trail faithfully records that “client X called this endpoint” — because that’s all a bearer token lets it know.
The handshake DPoP adds
DPoP doesn’t replace OAuth’s login handshake — it adds a binding step on top of it. The agent (or the broker acting on its behalf) generates a key pair before requesting a token, and every request from then on carries two things instead of one:
- The access token itself, now marked
token_type: DPoPinstead ofBearer, and cryptographically bound to the agent’s public key at issuance. - A DPoP proof: a short-lived, single-use JWT signed with the private half of that key, covering the HTTP method, the target URL, a timestamp, and (for resource-server calls) a hash of the access token being presented.
The resource server checks that the proof’s signature verifies against the public key the token was bound to, that the proof is fresh, and that it hasn’t been seen before. A copied access token, presented without the matching private key, fails that check immediately — the token is valid, but the party holding it isn’t the party it was issued to, and for the first time that distinction is something the protocol itself can see.
That’s the actual shift: proof of possession moves identity verification from a one-time event at login to a check performed on every single call. The same argument that kills refresh tokens for agents — that a login handshake built for a human who steps away doesn’t fit a principal that never does — cuts the other way here in the agent’s favor: an unattended agent can hold a private key continuously and sign a fresh proof on every request without ever bothering a human, something a browser-based flow could never ask of an actual person.
What this fixes that scoping and short TTLs don’t
Scoped, short-lived credentials and decision-token issuance already narrow what a leaked token can do and for how long. DPoP is orthogonal to both — it doesn’t shrink the blast radius of a leak, it removes the leak’s usefulness in the first place, for a specific and common class of theft:
- Log and transcript exfiltration stops working. The most realistic way an agent’s own token ends up somewhere it shouldn’t is the agent echoing its own context — a stack trace, a debug dump, a “here’s what I sent” transcript — into a place an attacker (or a second agent) can read it. With bearer tokens, that string is the whole secret. With DPoP, the leaked access token is useless without the private key, which never appears in request bodies, logs, or model context — it signs proofs locally and never leaves the process holding it.
- Token replay across delegation hops gets caught, not just discouraged. In an A2A chain where a task’s authority is supposed to flow forward through fresh, scoped grants rather than a forwarded token, a hop that tries to reuse someone else’s access token produces a DPoP proof it can’t forge — it doesn’t hold the bound key. What was previously a policy convention (“don’t forward tokens”) becomes something the resource server enforces cryptographically.
- The audit log gains a fact bearer tokens can’t give it. Today, tracing an incident back through an audit trail tells you which token acted, not which key holder did. With DPoP, every logged call carries a proof tied to a specific key — so “was this really agent X, or something holding agent X’s leaked token” stops being a question the log can’t answer.
Where it doesn’t help
DPoP proves the caller holds a specific private key. It says nothing about whether that caller should be allowed to do what it’s asking, whether the scope granted at mint time is still appropriate, or whether the agent itself has been compromised and is now signing proofs on an attacker’s behalf. A compromised agent process with its key intact will sign perfectly valid DPoP proofs for malicious requests all day — proof-of-possession authenticates the sender, not the sender’s intent. It’s a fix for token theft, not for agents gone wrong in general, and it composes with the broker layer rather than replacing it: the policy engine still has to decide what a request is allowed to do; DPoP just makes sure the request really came from who the token says it did.
The business case: proof of possession where the broker already sits
Fullmakt’s agent surface already replaces the bearer-token default with a
broker that mints short-lived, scope-pinned tokens on
client_credentials rather
than a forwardable session — DPoP is a natural extension of that same
posture, not a competing one:
- The private key never enters the agent’s own context. Because Fullmakt already sits between the agent and the credential — the client secret lives in the broker’s control plane, not the agent’s execution environment — the DPoP key pair lives in exactly the same place. There’s nothing for a poisoned tool response to trick the agent into leaking, because the agent never holds the key it would need to leak.
- Every mint and every call carries the same verifiable identity. The broker’s tamper-proof audit log already records which client, which collection, and what scope for every brokered call. Sender-constraining the token means that record now also proves the call really came from the key the broker bound at issuance — closing the one gap a bearer-token audit trail can’t close on its own.
- Delegation hops mint their own bound tokens instead of inheriting one. In an A2A management flow, each hop already gets its own scoped grant from the broker rather than a forwarded credential; a bound key per grant makes “this hop used a token issued to a different hop” something the resource server rejects outright, not just something a policy review might eventually catch.
- Revocation and proof verification happen at the same choke point. A disabled principal already fails at the broker’s next mint; a leaked but still-unexpired token now additionally fails at the resource server the instant it’s presented without the matching key — two independent checks covering the two different ways a stolen credential normally gets used.
The pattern underneath is the one that runs through everything Fullmakt does with the login handshake: an unattended agent can’t be trusted to hold a long-lived secret safely in its own context, so the broker holds the secret instead, and every property that would otherwise depend on the agent’s good behavior — scope, lifetime, and now proof of possession — gets enforced outside the process an attacker might one day control.
FAQ
What does DPoP actually stand for, and what problem does it solve? DPoP stands for Demonstrating Proof-of-Possession (RFC 9449). It solves the problem that a plain OAuth bearer token is valid for whoever holds it, regardless of whether they’re the party it was issued to — by binding the token to a key pair and requiring a fresh, signed proof of that key on every request.
Is DPoP the same thing as mTLS for API authentication? No. Mutual TLS binds a token to a certificate presented at the TLS connection layer, which usually requires PKI infrastructure and connection- level termination that can see client certs. DPoP works at the application layer — a JSON Web Token proof attached to each HTTP request — so it doesn’t require reworking the TLS termination path, which makes it considerably easier to add in front of an existing OAuth deployment.
Does DPoP replace short-lived, scoped credentials for AI agents? No, they solve different problems and stack together. Scoping and short TTLs limit what a leaked token can do and for how long. DPoP makes a leaked token unusable in the first place, for the common case where the token string itself — not the signing key — is what leaked.
If an agent’s whole process is compromised, doesn’t the attacker get the DPoP key too? Yes — DPoP defends against token theft, not full process compromise. If an attacker controls the process, they can ask it to sign proofs for their own requests. That’s a reason to keep the signing key (and the credential broker logic) out of the agent’s own execution context wherever possible, so compromising the agent doesn’t automatically hand over the key that would let a stolen token be reused elsewhere.
Does adding DPoP break existing bearer-token clients?
An authorization server can support both token types side by side —
token_type in the response tells the client which one it received, and
resource servers can require DPoP only for principals where it’s
configured. Rolling it out to unattended agents first, before touching
interactive human sessions, is the lower-risk order.