DPoP
TL;DR — A regular Bearer token is like a house key: whoever holds it can use it. DPoP (RFC 9449) binds each token to a public key the client holds and proves possession of on every request. Steal the token from a log or a network tap → useless without the private key. Additive: existing bearer clients keep working. This is the mental model — enablement steps live in Guides: Enable DPoP end-to-end and the spec-level depth in Security: DPoP.
The three-line explanation
- Client generates an asymmetric key pair (once, at startup).
- Every OAuth call includes a signed “proof” JWT saying “I’m making this specific request right now” — includes the request URL, method, and a fresh nonce.
- AuthPlane checks the proof and stamps the token with the client’s public key thumbprint (
cnf.jktclaim). The MCP server checks: does the proof’s key match the stamp in the token?
If someone steals just the token → useless (they don’t have the private key). If someone steals just a proof → useless (proofs are single-use and time-bound, ~60 seconds).
When it matters
- Multi-hop deployments — token transits multiple services. Any hop could log it or expose it. DPoP means a leak at hop 2 doesn’t compromise hop 3.
- High-security environments — compliance requires proof-of-possession.
- Environments where network traffic might be observable — VPCs shared with other tenants, transient debug proxies, dev environments that touch prod.
When to skip
- Local dev — everything on
localhost, single tenant. DPoP just adds complexity. - All communication over mTLS on trusted internal network — proof-of-possession already provided at a lower layer.
- Very short token expiry (< 5 min) — the risk window is small; the operational cost of managing DPoP keys may not be worth it.
DPoP costs ~1 ms per request in crypto. Proof is ~500 bytes. Real deployments barely notice.
The two headers to remember
On the request:
Authorization: DPoP <access_token> ← note "DPoP", not "Bearer"
DPoP: <proof-jwt>
On the token issued by AuthPlane (JWT payload):
{
"token_type": "DPoP",
"cnf": {
"jkt": "0iaeMlxbX3MR9k0DBjKBR9HBkfHTwIsuw_2dPjsLwBE"
}
}
cnf.jkt = base64url-encoded SHA-256 thumbprint of the client’s public JWK. Deep detail in Security: DPoP.
Additive — no big-bang migration
DPoP is opt-in per client. dpop.enabled: true at the AS side lets DPoP-capable clients get bound tokens; clients that don’t send a DPoP header still get regular bearer tokens. Roll out in stages:
- Enable AS-side (
dpop.enabled: true) — nothing breaks. - Advertise DPoP support in your MCP servers’ PRM (
inbound_dpop: {required: false}) — DPoP-capable clients start getting verified proofs. - Wait for all clients to migrate; monitor
authplane_dpop_proofs_validated_total. - Flip to
required: true— bearer-only requests now rejected.
The three switches every deployment needs
Real DPoP enforcement requires three things aligned:
- AS-side —
AUTHPLANE_DPOP_ENABLED=true. - SDK-side —
inbound_dpop: {required: true}(Python),inboundDPoP: {required: true}(TS),verifier.WithInboundDPoP(...)(Go). - Python-only extra step —
install_request_context(mcp)for the official MCP Python SDK.
Missing any one and either (a) DPoP isn’t enforced (fine for dev) or (b) fails closed with 401 invalid_dpop_proof (broken for the wrong reason). See Guides: Enable DPoP end-to-end.
The reverse-proxy trap
DPoP’s htu claim is the URL the client thinks it’s calling. The server derives its own htu from the incoming request. A reverse proxy that rewrites scheme or host without setting X-Forwarded-Proto / X-Forwarded-Host correctly breaks the comparison silently — every request fails invalid_dpop_proof even though the proof is fine.
Fix: configure proxy headers correctly. See Guides: Enable DPoP → reverse-proxy trap.
Why not just short-lived tokens?
Short-lived bearer tokens narrow the window of exposure but don’t eliminate the class of attacks. A bearer stolen in the first second of its 15-minute life is still usable for the rest. DPoP eliminates the class — even a bearer stolen at issuance time is worthless without the private key.
Belt and suspenders: DPoP + short-lived tokens is stronger than either alone.
Related
- Guides: Enable DPoP end-to-end — enablement steps + gotchas
- Security: DPoP — RFC 9449 §4.3 proof structure, algorithms, nonces, HTU derivation
- SDKs: Python — the PrefectHQ fastmcp caveat
- Reference: Configuration → DPoP — every knob
- Reference: Errors → DPoP codes
- Security: Threat model → T14/T15