DPoP (OAuth 2.0 Demonstrating Proof of Possession)
1. Overview
Definition: DPoP (Demonstrating Proof of Possession, RFC 9449) is a sender-constrained token mechanism in which an OAuth 2.0 client uses a public-private key pair at the application layer to prove, on every request, that "it is the owner of the key bound to the token," thereby preventing the reuse of a stolen token.
As OAuth 2.0 became widespread, the most persistent threat that remained was the structural weakness of Bearer tokens. A Bearer token is, as its name says, one where "whoever holds it is the authorized party." In other words, anyone who merely gets hold of the token can access the Resource Server as if they were the legitimate client. Even if the transport segment is protected with HTTPS, if the token leaks through logs, browser storage, proxy caches, malicious extensions, a man-in-the-middle, and so on, the attacker can replay that token as-is throughout its validity period. There is a mitigation of keeping the access token's lifetime short, but this is not a fundamental fix — it is closer to a symptomatic treatment that merely shrinks the exposure window.
The direction that confronts this problem head-on is binding the token to a specific key. If, when the token is issued, the client's public key thumbprint is engraved inside the token, and thereafter on every resource access the client must also present proof signed with the private key corresponding to that public key, then an attacker who steals only the token cannot create the proof without the private key, so the token is neutralized. This approach is called PoP (Proof of Possession) or a sender-constrained token, and previously an mTLS-based method (RFC 8705) existed as the standard. However, because mTLS requires a client certificate at the transport layer, the certificate information is tied to the TLS termination point (load balancer, CDN, reverse proxy), and it carried the substantial real-world constraint of being hard to apply to browser-based SPAs (Single Page Applications) or mobile public clients.
DPoP is an application-layer PoP technique that the IETF standardized as RFC 9449 in September 2023 precisely to fill this gap. Because it carries the proof in JWT form in an HTTP header rather than at the transport layer, it is unaffected by the TLS termination structure, and even in browsers the key can be generated and stored in non-extractable form via the Web Crypto API, making it especially well-suited to public clients. From a professional engineer's perspective, DPoP establishes itself as "a lightweight standard that, in a zero-trust era that assumes token theft (assume breach), blocks token reuse while also covering public clients."
2. Operating Principle and Components
2.1 Overall Structure
DPoP binds a token and a key via a public key thumbprint among three parties: the Authorization Server (AS), the client, and the Resource Server (RS). There are two core ideas. First, the client creates a DPoP Proof JWT by signing each request with a private key known only to itself. Second, inside the issued access token it places the thumbprint of the public key corresponding to that private key (cnf.jkt), binding the token and the key. On every request the Resource Server checks whether "the thumbprint engraved in the token" and "the thumbprint of the public key contained in the proof" match.
flowchart LR
C["Client (holds private key)"]
AS["Authorization Server (AS)"]
RS["Resource Server (RS)"]
C -->|"① Token request + DPoP Proof (includes public key)"| AS
AS -->|"② DPoP-bound token (cnf.jkt = public key thumbprint)"| C
C -->|"③ Resource request + access token + DPoP Proof (includes ath)"| RS
RS -->|"④ Resource response on successful thumbprint match"| C
AS -.->|"Engrave key thumbprint"| RS
In this structure the root of trust is the single assumption that "only the client holds the private key." The public key, token, and proof can all travel the network, but the only party that can create a signature with the private key is the legitimate client, so the server can be confident, from signature verification alone, that "the one who sent this request is the owner of the key bound to the token." Therefore the secure storage of the private key (non-extractable key, secure hardware) effectively underpins all of DPoP's security.
The client first creates a key pair (①) and attaches a DPoP Proof carrying the public key (JWK) to the token request. The AS computes the RFC 7638 SHA-256 thumbprint of the public key, engraves it as jkt into the access token's cnf (confirmation) claim, and returns the token (②). Thereafter, on every resource access the client signs a fresh Proof with the same private key and sends it along (③), and the RS releases the resource only after verifying that the public key thumbprint of the Proof matches the token's cnf.jkt (④). The token type also changes from Bearer to DPoP, and the request headers carry both Authorization: DPoP <token> and DPoP: <proof-jwt>.
2.2 Structure of the DPoP Proof JWT
A DPoP Proof is a short-lived JWT newly generated on every request. The JWT's protected header contains typ: "dpop+jwt", the signing algorithm (alg), and the entire public key (jwk). As an important security rule, this header must carry only the public components of the public key, and if private-key material is mixed in, the verifier must reject it immediately (RFC 9449 §4.2). Thanks to the design of including the public key in every request, the server can extract the key from the proof itself and verify without any separate key registration or lookup.
The payload (claims) contains values that bind the request to a specific context. Looking at each element together with its rationale: htm (HTTP method) and htu (HTTP URI) nail down which method targets which endpoint, preventing a proof meant for one request from being reused against another endpoint. iat (issued at) is the issuance time, serving as the basis for the server to filter out stale proofs that fall outside the allowed time window. jti (JWT ID) is a unique identifier for each proof, letting the server remember recently seen jti values and detect replay of the same proof. At the resource-request stage the ath (access token hash) claim is added — the base64url(SHA-256(access_token)) value of the presented access token — which additionally binds the proof to a specific token. If the server demands a nonce, a nonce claim is also included.
| Location | Field | Meaning | Security role |
|---|---|---|---|
| header | typ |
fixed dpop+jwt |
prevents type confusion (JWT mix-up) |
| header | jwk |
public key (public components only) | server verifies signature on the spot |
| payload | htm/htu |
HTTP method/URI | dedicates proof per endpoint |
| payload | iat |
issuance time | blocks proofs past the allowed window |
| payload | jti |
unique proof ID | replay detection (server cache check) |
| payload | ath |
access token hash | binds proof↔token (on resource request) |
| payload | nonce |
server-provided nonce | server-driven replay prevention; no clock sync needed |
2.3 Key Binding and the Nonce Mechanism
The binding of token and key is done via the single pair of values cnf.jkt. The AS computes the RFC 7638 SHA-256 thumbprint of the Proof's jwk and plants it in the access token as the jkt (JWK SHA-256 Thumbprint) field inside the cnf (confirmation method) claim defined by RFC 7800. The Resource Server recomputes the same thumbprint from the public key of the incoming Proof and compares it byte-for-byte with the token's cnf.jkt (RFC 9449 §7). If the two values differ, that token was not presented by the owner of this key and is rejected. If the access token is an opaque token rather than a JWT, the RS obtains the same cnf.jkt from the token introspection (RFC 7662) response and compares.
The nonce is a device that makes the server drive replay prevention. If the client proves freshness with iat alone, the server must trust the client clock, and replay room remains for the span of the allowed time window. To eliminate this, the server hands down a server-generated nonce via the DPoP-Nonce response header, and the client must place that value verbatim into the nonce claim of the next Proof. If the server demanded a nonce but it is missing or stale, the server returns HTTP 401 together with a use_dpop_nonce error (RFC 9449 §8), and the client immediately retries with the new nonce. Practical implementations usually issue and rotate a 32-byte base64url nonce with a short TTL of around 5 minutes, filtering out every proof carrying a nonce the server never issued.
2.4 Concrete Example: Headers Carrying an Actual Request
Seeing the concept together with concrete values makes the structure clear. An actual HTTP request in which a client calls GET /userinfo with access token eyJ... looks roughly as follows. The key point is that the scheme of the Authorization header is not Bearer but DPoP, and beneath it the proof JWT is carried together in a separate DPoP header.
GET /userinfo HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJhbGciOiJSUzI1NiIsImtpZCI6...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7...
Decoding the DPoP header here, the protected header is {"typ":"dpop+jwt","alg":"ES256","jwk":{...public key...}}, and the payload is {"jti":"-BwC3ESc6acc2lTc","htm":"GET","htu":"https://api.example.com/userinfo","iat":1762041600,"ath":"fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo","nonce":"eyJ7S_..."}. The Resource Server verifies the signature with jwk, then checks in turn whether htm and htu match the actual request, whether ath equals the SHA-256 hash of the presented token, and whether the public key thumbprint matches the token's cnf.jkt. The jti is, for example, retained in a cache for a 120-second allowed window, and if the same value arrives again it is judged a replay. The cost of carrying the public key on every request (a few hundred bytes for ES256) may seem puzzling, but it is a design trade that lets the server verify statelessly without prior key registration or lookup.
3. Processing Procedure (Request Lifecycle)
DPoP's security hinges on what is verified at each stage. The flow below shows the verification gates that a single resource request must pass.
sequenceDiagram
participant C as Client
participant RS as ResourceServer
C->>C: "Sign Proof JWT with private key (htm,htu,iat,jti,ath,nonce)"
C->>RS: "Authorization: DPoP token + DPoP: proof"
RS->>RS: "① Signature verify (confirm Proof signature with jwk)"
RS->>RS: "② Do htm/htu match the actual request"
RS->>RS: "③ iat time window / jti duplicate (replay) check"
RS->>RS: "④ Confirm ath = SHA-256(token)"
RS->>RS: "⑤ jwk thumbprint == token cnf.jkt"
alt All checks pass
RS-->>C: "200 resource returned"
else Nonce required / mismatch
RS-->>C: "401 use_dpop_nonce + DPoP-Nonce"
C->>RS: "Re-sign Proof with new nonce and retry"
end
Signature verification (①) is the root of the proof's own integrity and of the key-ownership proof. The htm/htu check (②) guarantees the proof targets this very endpoint now, preventing cross-endpoint reuse. The time-window and jti check (③) blocks replay that uses the same proof twice; the server typically retains seen jti values in an in-memory store (e.g., Redis) for an allowed window of tens of seconds to a few minutes. The ath confirmation (④) binds the proof to be used with exactly this access token, and finally the thumbprint comparison (⑤) confirms the ownership match between token and key. If any one of the five gates fails, the resource is not released.
One operational point to note is binding of the refresh token. In public clients, not only the access token but also the refresh token is bound to the DPoP key, so even if the refresh token leaks, a new access token cannot be obtained without the key. By contrast, a confidential client (backend) already has a client authentication means, so the roles are split such that the refresh token is bound by the client credential and the access token by the DPoP key.
4. Comparison: Bearer · mTLS Binding · DPoP
The difference among the three methods splits along "what the token is bound with" and "at which layer the proof is made." Bearer binds nothing, so it is defenseless against theft; mTLS binding binds strongly with a transport-layer certificate but is hard to handle in public clients and TLS termination structures. DPoP binds with an application-layer key to cover even public clients, but the essential trade-off is that it does not protect the transport segment itself as much as mTLS does. That is, DPoP blocks "reuse after theft," but it does not fully block a sophisticated MITM that relays an active session in real time (hence HTTPS and nonce are prerequisites).
| Category | Bearer | mTLS binding (RFC 8705) | DPoP (RFC 9449) |
|---|---|---|---|
| Binding medium | none | client TLS certificate | application-layer key pair |
| Proof layer | - | transport (TLS) | application (HTTP header) |
| Public-client suitability | high | low | high (browser, mobile) |
| Impact of TLS termination structure | none | large (certificate tied to termination point) | none |
| Reuse of stolen token | vulnerable | blocked | blocked |
| Main cost | - | PKI, infrastructure | per-request signing, nonce round-trip |
The practical implication is clear. In environments such as server-to-server (B2B) communication where PKI is already in place and TLS termination is directly controlled, mTLS binding is robust. Conversely, in public APIs accessed by countless browsers and mobile apps — especially where public clients handle high-value tokens, as in open banking and MyData in the financial sector — DPoP is the realistic choice. In fact, the UK Open Banking and the FAPI 2.0 (Financial-grade API) security profiles allow both mTLS and DPoP as sender-constraining means, letting one choose to fit the environment.
4.1 The Effect Seen Through a Concrete Attack Scenario
The difference becomes more convincing when seen through a scenario with concrete numbers. Assume a service that sets the access token lifetime to 15 minutes. In the Bearer approach, if an attacker exfiltrates the token through logs or XSS and sends it to their own server, for the remaining validity (up to 15 minutes) they can replay that token as-is from any device to access the resource. Reducing the token lifetime to 1 minute narrows the window, but token re-issuance grows more frequent by that much, worsening AS load and user experience. That is, in Bearer, lifetime shortening is a zero-sum trade between security and cost.
Let us see how DPoP neutralizes the same attack. Even if the attacker exfiltrates the token, the Resource Server confirms that the signature of the Proof JWT attached to the request cannot be made without the private key the attacker does not have. Even if the attacker also steals the victim's past Proofs to reuse them, they are rejected if htm/htu point to a different endpoint, and even at the same endpoint replay is blocked by jti duplication, the iat time window, or a nonce mismatch. As a result, DPoP structurally closes the most common theft path of "external reuse after exfiltration" without forcibly shortening the token lifetime, easing the zero-sum between security and usability.
5. Deep Dive: Standardization Trends and Application Cases
As DPoP was finalized as RFC 9449 in September 2023, it became not a "proposal" but a citable standard, and security profiles quickly embraced it thereafter. Representatively, the FAPI 2.0 Security Profile requires access tokens to be issued as sender-constrained and designates mTLS or DPoP as the means. This is a trend that effectively bans Bearer in high-risk domains — such as open banking and MyData, where a single token amounts to an account/personal-information access right — and Korea's financial-security guidance is following the same direction.
The implementation ecosystem has also matured. On the server side, Spring Security (DPoP-bound Access Token support since 6.x), Keycloak, Curity, Authlete, and others have built in issuance and verification, and on the client side the pattern of creating a non-extractable key via the browser's Web Crypto API and storing it in IndexedDB has taken hold. A non-extractable key makes it impossible for JavaScript to read the private key's raw material, so even if a token leaks via XSS it is hard for an attacker to take the key and forge a proof on another device. However, since this does not prevent an attack in which XSS calls the proof-generation function within the same browser to perform requests by proxy, DPoP must be understood not as an XSS countermeasure but as a means to prevent external reuse after token exfiltration.
DPoP is highly pertinent in the Korean context as well. MyData (the business of managing one's own credit information) is structured to receive, by API, a data subject's personal credit information from financial institutions upon the subject's delegation, so a single transfer token amounts to a broad personal-information access right. When such a token is handled in a public client (a mobile app), using Bearer as-is poses a large risk of token reuse upon device theft or app cloning. Applying sender constraint means that even if a token leaks, a third party without the key cannot complete a transfer request, so the trend of Korea's financial sector requiring PoP-family methods for high-value APIs strengthens the rationale for adopting DPoP.
Likely exam directions include "the limits of Bearer tokens and a comparison of sender-constrained tokens," "the pros and cons of DPoP versus mTLS binding and the criteria for choosing," "token-protection design in high-risk APIs such as open banking/MyData," and "blocking token reuse from a zero-trust perspective." As an answer-composition strategy, a persuasive structure is to ① first present the structural weakness of Bearer, ② then develop DPoP's key-binding, proof, and nonce mechanisms with diagrams, ③ organize the selection criteria via a comparison table with mTLS binding, and ④ conclude with FAPI 2.0 / open-banking application and public-client suitability.
6. Considerations and Implications
Application strategy (gradual adoption): If it is hard to change an existing Bearer-based system all at once, a realistic gradual transition is to apply DPoP starting from high-risk endpoints (transfers, personal-information lookups) and separate the token type as
DPoPso that the Resource Server varies the binding requirement level per endpoint. Since the simultaneous support of AS, RS, and client SDK is a prerequisite, a design that handles DPoP verification commonly at the API gateway to minimize changes to backend services is effective.Trade-off (security vs. performance/complexity): DPoP requires a private-key signature per request, multi-stage verification by the server, and a nonce round-trip (one extra round-trip at first). In high-TPS environments, the storage cost of the
jtireplay cache and the CPU load of signature verification can become real bottlenecks, so the nonce TTL, allowed time window, and cache expiration policy must be tuned between latency and security. Delegating key signing to a non-extractable key or hardware (TPM, Secure Enclave) raises safety but further constrains throughput.Outlook (zero trust / PoP becoming the default): As zero trust that "does not trust the network perimeter" spreads, the Bearer assumption of "token possession = authority" is gradually being retired, and the trend is for sender constraint to become the default (secure by default). Just as FAPI 2.0 mandated it, future high-value API standards are likely to require PoP as a necessity rather than an option.
Related technologies (ecosystem integration): DPoP is not complete on its own; defenses overlap when it is combined with PKCE (preventing authorization-code interception), the BFF pattern (an alternative that hides the token itself from the browser), token introspection (confirming the
cnfof opaque tokens), and short token lifetimes. In SPA security in particular, "a BFF that keeps no token in the browser at all" and "DPoP that keeps the token but binds it with a key" are complementary options, so the combination must be designed to fit the service's architecture and operational capability.Interoperability and operational maturity: Because DPoP works only when the AS, RS, and client agree on the same algorithm (
ES256, etc.) and nonce policy, in a multi-party ecosystem the supported algorithms, whetherDPoP-Nonceis required, and the allowed clock-skew window should be codified into an interoperability profile to reduce field failures. Also, considering that an existing token'scnf.jktis invalidated on key rotation, standardizing the re-authentication/re-binding procedure and error (use_dpop_nonce,invalid_dpop_proof) handling logic into the client SDK is the key to operational stability.Clear recognition of the limits: DPoP is a tool to prevent the external reuse of a token; it is not an all-purpose means to block proxy calls from inside an XSS-compromised or malicious device, a real-time relay MITM, or a complete takeover of the user's own terminal. Therefore it must be combined with content security policy (CSP), input validation, terminal integrity checks, and the like in defense in depth for its value to be whole.
References
- RFC 9449, OAuth 2.0 Demonstrating Proof of Possession (DPoP): https://www.rfc-editor.org/rfc/rfc9449
- RFC 7638, JSON Web Key (JWK) Thumbprint: https://www.rfc-editor.org/rfc/rfc7638
- RFC 8705, OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens: https://www.rfc-editor.org/rfc/rfc8705
- Spring Security Reference, OAuth 2.0 DPoP-bound Access Tokens: https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/dpop-tokens.html
- FAPI 2.0 Security Profile (OpenID Foundation): https://openid.net/specs/fapi-2_0-security-profile.html
In one line: DPoP (RFC 9449) is a sender-constrained token standard that, by having the client match a Proof JWT signed with a private key against the public key thumbprint (cnf.jkt) engraved in the token on every request, blocks reuse of a stolen OAuth token even in public clients without depending on the transport layer.