
Presented at Keycloak DevDay 2026. Built into the Keymate stack.
Estimated read: 7–8 minutes
A bearer token works like a banknote. The API server honors whoever presents it, no questions asked. If an attacker gets your token through XSS, log leakage, or a network interception, they get full authorized access from any device. The server has no way to tell the real holder from the thief.
Three things make this dangerous:
Bearer tokens were designed for simplicity. That simplicity became a problem.

DPoP stands for Demonstrating Proof-of-Possession (RFC 9449). It adds three layers of protection that bearer tokens lack.
The client generates an ES256 key pair. During token issuance, the authorization server (Keycloak) computes the SHA-256 thumbprint of the public key and embeds it inside the access token as a cnf.jkt claim. From that moment, the token is bound to that specific key pair. A stolen token is useless without the matching private key.
On every request, the client signs a short-lived proof JWT that includes a unique identifier (jti) and a timestamp (iat). The resource server caches each jti and rejects duplicates. An intercepted proof cannot be replayed.
Each proof also includes the HTTP method (htm) and the target URL (htu). A proof created for GET /api/users cannot be used for POST /api/admin. A captured proof is scoped to exactly one endpoint and one moment in time.
Relying on bearer tokens creates a security debt: a single compromised log or XSS vulnerability can bypass months of hardening. DPoP changes the model by shifting from bearer to proof-of-possession. For teams building modern enterprise identity or targeting FAPI 2.0 compliance, this is the foundation.
| Feature | Bearer Tokens | DPoP Tokens |
|---|---|---|
| Security Model | Bearer (no proof) | Proof-of-Possession |
| Theft Risk | High (usable by anyone) | Low (sender-constrained) |
| Complexity | Low | Medium |
| Compliance | OAuth 2.0 Baseline | FAPI 2.0 / High-Security |
Bearer tokens are cash. DPoP tokens are credit cards with a PIN and per-transaction limits.
Keycloak (26.4+) has solid authorization-server support for DPoP:
cnf.jkt claim during token issuance.Keycloak covers the authorization server side. For your own APIs, microservices, and gateways, you need to handle the resource server side yourself:
cnf.jkt, and enforce htm/htu binding yourself.jti caching across distributed pods requires a multi-layer cache strategy.These are the gaps we filled.
Our Admin Console uses a Backend-for-Frontend architecture. The browser communicates with the BFF via session cookies. The BFF communicates with Keycloak and downstream APIs using DPoP-bound tokens.
This setup is ideal for DPoP because the private key never leaves the server. It gets generated during login, serialized to JWK format, and stored in a Redis session alongside the access token. On every API call, the BFF reads the key and token in a single Redis round-trip, generates a fresh proof, and attaches it to the outgoing request.
A DPOP_ENABLED feature flag controls the entire flow. Set it to false and everything reverts to bearer tokens. No code changes, no redeployment. We used this to roll out DPoP incrementally across environments.
Error handling follows the RFC closely: if the resource server returns an invalid_dpop_proof error, the interceptor generates a fresh proof and retries the request once. If DPoP keys are missing from the session for any reason, the system gracefully falls back to bearer.
Our JS SDK (@keymate/access-sdk-js) provides a DPoPManager that handles proof generation for any JavaScript runtime:
generateKeyPair(extractable?) creates an ES256 key pair via the Web Crypto API. Pass extractable: true for server-side use (keys can be serialized to JWK for Redis). Pass extractable: false for browsers, where the private key becomes a non-extractable CryptoKey handle that JavaScript cannot read, only use for signing.generateProof(method, url, accessToken, keyPair) produces a standards-compliant DPoP proof JWT with jti, iat, htm, htu, and ath claims.type: 'DPoP', the SDK's HTTP client generates and attaches a fresh proof to every outgoing request. No manual wiring needed.The SDK works in browsers, Node.js, Deno, Bun, and Cloudflare Workers. Anywhere the Web Crypto API is available.

Our Access Gateway validates every DPoP proof from the Admin Console before the request reaches backend services (Keycloak Admin REST API, microservices).
The flow: the gateway triggers a token introspection call to Keycloak, extracts the cnf.jkt from the response, matches it against the proof's key thumbprint, and checks the jti against a remote Infinispan cache for replay protection. If anything fails, the request gets a 401.
This is how we protect our own services. But we also wanted to make DPoP validation available to everyone.

We built an open-source APISIX plugin so any team can add DPoP validation to their API gateway. The plugin:
cnf.jkt, either from local JWT verification or via Keycloak token introspection.cnf.jkt.jti against an L1/L2 hybrid cache (in-memory + Redis/Infinispan) to prevent replay.Your backend services receive a normal bearer token and require zero changes. DPoP validation happens entirely at the edge.
In a BFF architecture, keys live in a server-side session. Problem solved. But what about single-page applications that talk directly to APIs?
SPAs need to store the key pair in the browser. The recommended approach:
IndexedDB + non-extractable CryptoKey. Generate keys with extractable: false. The Web Crypto API stores the private key as an opaque handle. JavaScript can use it for signing but cannot read the raw key material. Store the CryptoKey object in IndexedDB for persistence across page reloads.
Alternatives exist but come with trade-offs. sessionStorage loses keys on tab close and exposes them to XSS. In-memory storage is the most secure against persistence attacks but loses keys on every page refresh.
DPoP does not eliminate XSS. If an attacker can execute JavaScript in your page, they can use the CryptoKey handle to sign proofs. But they cannot extract the private key for use on another device. DPoP raises the bar from "steal a token string" to "maintain persistent code execution in the victim's browser session."
We presented three proposals at Keycloak DevDay 2026 to make DPoP adoption easier for everyone:
1. Reference Resource Server Validation Library. Keycloak handles the authorization server side well. But every team building custom APIs has to implement proof validation from scratch. A reference library (Java/JS) from the Keycloak ecosystem would lower the barrier a lot.
2. Case-Insensitive DPoP Handling. Some Keycloak code paths treat the DPoP auth-scheme as case-sensitive. RFC conventions say auth-schemes should be case-insensitive. We proposed standardizing this in DPoPUtil and the introspection provider.
3. DPoP-Aware Token Exchange (RFC 8693). When a subject token is DPoP-bound, the exchange request should require a matching DPoP proof and verify that the proof's key thumbprint matches the token's cnf.jkt, rather than rejecting the request outright. Keycloak 26.6 will block token exchange for DPoP-bound tokens. We proposed a validation-based approach where presenter binding survives token exchange flows instead of breaking them.
We think DPoP should be a first-class concern in the OAuth ecosystem, not something each team has to figure out on their own.
Keymate builds identity infrastructure that goes beyond authentication. If you are looking into sender-constrained tokens for your stack, we would love to talk.
Learn how Keymate can help you implement DPoP across your stack, from BFF to gateway.
Stay updated with our latest insights and product updates