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

  1. Client generates an asymmetric key pair (once, at startup).
  2. 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.
  3. AuthPlane checks the proof and stamps the token with the client’s public key thumbprint (cnf.jkt claim). 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:

  1. Enable AS-side (dpop.enabled: true) — nothing breaks.
  2. Advertise DPoP support in your MCP servers’ PRM (inbound_dpop: {required: false}) — DPoP-capable clients start getting verified proofs.
  3. Wait for all clients to migrate; monitor authplane_dpop_proofs_validated_total.
  4. Flip to required: true — bearer-only requests now rejected.

The three switches every deployment needs

Real DPoP enforcement requires three things aligned:

  1. AS-sideAUTHPLANE_DPOP_ENABLED=true.
  2. SDK-sideinbound_dpop: {required: true} (Python), inboundDPoP: {required: true} (TS), verifier.WithInboundDPoP(...) (Go).
  3. Python-only extra stepinstall_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.