Prompt and scope
You own an OAuth API used by mobile and browser clients. The resource server currently validates only Bearer access tokens; one log leak could let an attacker replay requests while the token is valid. Design a DPoP (Demonstrating Proof-of-Possession) scheme, including the authorization server, client, resource server, and behavior when DPoP is unavailable.
This is a backend security architecture question, not a vendor-configuration quiz. RFC 9449 binds an access token to a client public key and requires each call to carry a proof bound to the HTTP method and URI. RFC 9700 adds current OAuth security guidance such as avoiding weak flows and using PKCE.
What the interviewer is testing
A strong answer starts with the Bearer-token failure mode: anyone holding the string can use it, so a stolen token is indistinguishable from a legitimate request. It then gives an end-to-end flow: the client creates a key, the token endpoint verifies a DPoP proof and binds the public key, and the resource server checks the signature, method, URI, token hash, and time window.
The interviewer also expects a plan for jti replay, server nonces, lost keys, URI rewriting by proxies, refresh tokens, old clients, and telemetry. Saying “sign the JWT” is insufficient: a normal JWT signature proves issuer integrity, not that the caller still possesses the private key used at issuance.
Questions to clarify first
Client and threat model
Confirm whether the client is a browser SPA, native mobile app, or server application. Whether the private key can live in hardware-backed storage and whether every installation gets its own key changes the lifecycle. Ask whether the attacker can read logs, proxies, browser storage, or device files, and whether they can intercept requests in real time.
Resource-server boundary
Confirm gateways, TLS termination, and internal forwarding. DPoP htu must match the URI semantics verified by the resource server. If a gateway rewrites paths, define canonicalization and trusted forwarding headers; do not let a client sign one address while the backend verifies another.
Availability and migration constraints
Ask whether legacy Bearer clients must continue working, whether a short dual-stack period is acceptable, and whether login and refresh use the same authorization server. If regulation or a high-value API requires strong sender constraint, make DPoP mandatory. For lower-risk resources, migrate through capability negotiation and measured rollout.
30-second answer framework
“I would design DPoP as a three-party protocol. The client generates a key pair per installation and sends a one-time DPoP proof during the token request. The authorization server verifies it and binds the access token’s cnf.jkt to the public key. Every API call carries a fresh DPoP JWT; the resource server checks its signature, htm, htu, iat, unique jti, ath, and the token’s cnf.jkt, with a nonce and short time window to reduce replay. A lost private key revokes the bound tokens. Legacy clients get a time-limited Bearer path only for explicitly low-risk resources while high-value operations move to mandatory DPoP.”
Step-by-step solution
Step 1: Create the key and bind authorization
On first run, the client creates an asymmetric key pair and keeps the private key in secure device storage. It sends the public key in the JWK carried by a DPoP proof when requesting a token. The authorization server validates the proof’s signature, type, and time, then binds the token’s confirmation data to the public key’s JWK thumbprint. With an authorization-code flow, still use RFC 9700’s PKCE guidance: DPoP proves token possession and does not replace code protection.
Step 2: Generate a proof per request
For every request, the client creates a fresh JWT containing at least jti, htm, htu, and iat, then signs it with the bound private key. It puts a hash of the access token in ath, allowing the resource server to verify that the proof targets this token rather than another token signed by the same key. The proof goes in the DPoP header, and the access token uses the DPoP authorization scheme instead of Bearer.
Step 3: Verify in a fixed order
The resource server first restricts algorithms and JWK types, then verifies the JWT signature, key format, time window, method, and URI. It hashes the access token and compares the result with ath; it reads cnf.jkt from the token and compares it with the proof key thumbprint. It then checks whether the jti has appeared in the current window before applying ordinary scope, audience, and resource authorization.
Step 4: Handle nonces and replay
The server can issue a nonce for high-risk resources, requiring the client to include it in the next proof. jti deduplication is needed within the acceptance window. A single-node cache works for a small deployment; multi-instance resource servers need an expiring shared cache, or must explicitly accept a short-window duplicate risk and record it as a security event. Rejection telemetry should distinguish expiry, bad signatures, thumbprint mismatch, and missing nonce without exposing key material.
Step 5: Design rotation, revocation, and fallback
If a device is lost, a private key is exposed, or the server detects abuse, revoke access and refresh tokens associated with that thumbprint and require authorization again. Rotation cannot be a silent client-only replacement: the new public key needs a new authorization binding. During migration, a resource server may accept both schemes, but high-value writes should have a separate mandatory-DPoP policy so compatibility never becomes the security default.
Step 6: Compare mTLS with plain Bearer
mTLS places proof at the TLS client-certificate layer and fits server-to-server systems with mature certificate operations. DPoP works at the application layer and fits mobile and browser clients, but it adds proof, nonce, jti, and key-storage operations. Plain Bearer is easiest to deploy but cannot resist replay after the token string is copied. Choose based on client capabilities, proxy topology, compliance, and operating cost rather than enabling DPoP blindly for every endpoint.
High-quality sample answer
I would begin with the threat model: a log, proxy, or client-storage leak can expose an access token, after which an attacker can call the API during its validity period. Shorter expiry narrows the window but does not prove that the caller is the original client.
The client creates a key pair per installation and protects the private key. During the authorization-code exchange I require PKCE and a DPoP proof; the authorization server verifies the proof and binds the token’s cnf.jkt to the public key. At the resource server, I verify the proof signature, htm, htu, iat, jti, ath, and nonce in a fixed order, then compare the proof key thumbprint with the token binding. Any mismatch is unauthorized and is classified in security metrics.
I store jti values in a TTL-backed shared deduplication cache for multi-instance deployments. High-risk writes require nonce and DPoP. During migration, low-risk reads may be dual-stack, but the deadline is tied to client version and resource risk. A lost key revokes its tokens and starts a new authorization; a Bearer fallback never shares the high-value write path. Finally, I run integration tests for forged proofs, old jti replay, URI changes, wrong token hashes, expired nonces, and proxy rewrites, then monitor rejection rate, nonce retries, and abnormal thumbprints to prove replay is being blocked.
Common mistakes
- Mistake: Only signing the access token as a JWT. → Why it fails: The signature proves issuer integrity but not possession of the client private key. → Fix: Bind
cnf.jktand verify a fresh DPoP proof on every request. - Mistake: Reusing one DPoP proof. → Why it fails: An attacker can replay the complete request because
jtiand time checks cannot distinguish it. → Fix: Generate a newjtiper request and deduplicate it within the acceptance window, adding a nonce when needed. - Mistake: Verifying DPoP only at the gateway while internal services trust an unbound user header. → Why it fails: Forwarding can lose method, URI, or token binding and let a forged identity through. → Fix: Pass a normalized verification result across a trusted boundary and restrict who can set it.
- Mistake: Falling back to Bearer on every validation error. → Why it fails: An attacker can intentionally trigger DPoP errors to reach the weakest path. → Fix: Allow a time-limited fallback only for registered legacy clients and low-risk resources.
Follow-up questions and responses
Follow-up 1: How do you deduplicate jti across regions?
Choose consistency by resource risk. High-value writes can use a cross-region shared store and pay one extra round trip; low-risk reads can use regional caches and a short window, recording duplicates without sacrificing availability. In either design, write the window, failure behavior, and duplicate rate into the SLO.
Follow-up 2: What if XSS reads the browser private key?
DPoP cannot repair a client execution environment that is fully controlled. Prefer non-exportable keys, a strict content-security policy, short-lived tokens, and refresh rotation; revoke the binding when the thumbprint behaves abnormally. State the protection boundary clearly: DPoP targets a copied token when the private key was not copied, not every form of endpoint takeover.
Follow-up 3: A proxy changes the URI. How do you verify htu?
Define a mapping from the external canonical URI to the internal route, and allow only a trusted proxy to provide authenticated original scheme, host, and path. The resource server applies the same canonicalization before comparison and rejects client-controlled, unauthenticated forwarding headers. If no trusted mapping exists, terminate DPoP at the boundary and issue a short-lived internal identity instead of guessing the original URI.
Follow-up 4: Legacy clients can only send Bearer. Can you support them forever?
Permanent compatibility is not a security design. Set a migration deadline by client version, user risk, and resource type; require DPoP first for high-value writes, return an actionable upgrade error, and monitor residual traffic. Keep a restricted Bearer route only when a risk review justifies it and adds device binding or rate limits.