skip to content

Why would an OAuth 2.0 client send `dpop_jkt` on its authorization request rather than only proving its key at the token endpoint?

level: middleimportance: nice to knowfreq 19%

answer

  1. bind early, not at redemption
  2. a thumbprint on the authorization request
  3. the code, not only the token
  4. token endpoint recomputes and compares
  5. mismatch means the request is rejected

basics

~20 s

Sending dpop_jkt binds the authorization code to a named key at the moment the code is issued, not when it is redeemed. The token endpoint must compare the proof's key thumbprint against it and reject a mismatch, so only that key's holder can redeem the code.

solid answer

~40 s

`dpop_jkt` is an authorization-request parameter carrying the base64url-encoded RFC 7638 JWK SHA-256 Thumbprint of the key the client intends to use. It declares the key up front, so the authorization code is stamped with it at issuance. When the client later redeems that code, the authorization server computes the thumbprint of the public key in the DPoP proof's `jwk` header and MUST reject the token request if it does not match the recorded `dpop_jkt`. Without the parameter, the first moment any key is involved is the token request — and whoever makes that request picks the key, so a code that reached the wrong party could be redeemed against that party's own key.

code

http · 7 lines
http
GET /authorize?response_type=code
  &client_id=s6BhdRkqt3
  &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
  &scope=commissioning%3Awrite
  &state=Vf9c2XhKp
  &dpop_jkt=NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs HTTP/1.1
Host: as.example.org

go deeper

for a junior

Recall that the parameter is a fingerprint of a key, sent at the start of the flow, and that it is a declaration of intent rather than any kind of secret.

for a middle

Explain the timing: the code is stamped with the key when it is issued, and at redemption the server recomputes the thumbprint from the proof's key and must reject a mismatch.

for a senior

Bring the operational consequence: the key pair must exist before the authorization request is composed, and a key regenerated between the two requests produces a rejection that presents itself as a broken sign-in.

for a principal

Judge where early binding is worth the client-side key-lifetime obligation it imposes, and which client shapes in an estate can honestly keep one key across the whole flow.

## What the parameter is `dpop_jkt` is an **authorization-request** parameter defined by RFC 9449. Its value is a single string: the base64url-encoded RFC 7638 JWK SHA-256 Thumbprint of the public key the client intends to prove possession of later. It travels in the front channel alongside `response_type`, `client_id`, `redirect_uri`, `scope` and `state`, and it is the one place in the whole mechanism where the **client** sends a thumbprint — everywhere else the client sends the key and the authorization server computes the digest. It proves nothing by itself. A thumbprint is a public value, and anyone who has seen the key can compute it. What it does is **declare**, before the user is ever redirected, which key the eventual token request will have to sign with. ## Two artefacts, two moments Key binding in this mechanism attaches to two different things, at two different points in the flow, and conflating them is the usual confusion. | artefact | bound to the key by | at which moment | |---|---|---| | the authorization code | `dpop_jkt` on the authorization request | when the code is issued | | the issued access token | the `cnf.jkt` claim, minted from the proof | when the code is redeemed | The two values are normally identical strings, because they are thumbprints of the same key. They are not the same field, they are not sent by the same party, and one is a request parameter while the other is a claim inside the token. ## What the token endpoint must do When a code was issued against an authorization request that carried `dpop_jkt`, the authorization server, at redemption: 1. validates the DPoP proof presented with the token request; 2. computes the JWK SHA-256 Thumbprint of the public key in the proof's `jwk` header; 3. compares it with the `dpop_jkt` value recorded against that code; 4. **MUST reject the token request on a mismatch.** That obligation is the whole value of the parameter. Without step 4 the declaration is decoration. ## Why binding the code earlier matters If the key first appears at the token request, then whoever makes the token request chooses the key. A code that reaches a party other than the one that began the flow can be redeemed by that party using a key it generated itself, and the access token it receives is bound — correctly and usefully — to **its** key. The binding is intact; it simply protects the wrong holder. `dpop_jkt` moves the decision one step earlier. The code is stamped at issuance with a key the client had already committed to, so redeeming it requires signing a proof with the matching private half. A code alone stops being enough. This matters most where the client has no confidential credential to fall back on at the token endpoint, because there the code is otherwise the only thing standing between a request and a token. ## What it is not - **Not client authentication.** A thumbprint identifies a key; it does not establish who the client is. Authentication of the client at the token endpoint is a separate matter with its own methods. - **Not a proof.** Possession is demonstrated at the token endpoint by a signature under the private key, not by the declaration on the authorization request. - **Not the `cnf` claim.** One is a parameter the client sends; the other is a claim the authorization server mints into the token it issues. - **Not confidential.** It travels in a front-channel query string, is a digest of a public key, and is meant to be visible. ## The ordering consequence people trip over Because the thumbprint must be known before the authorization request is composed, the client has to settle its key pair **before** it sends the user to the authorization endpoint. A client that would rather create a key lazily, at the moment it first needs to sign a proof, has to be restructured. Worse, a client that discards and regenerates its key between the authorization request and the token request — on a restart, a fresh process, or a change of storage — produces a proof whose thumbprint cannot match, and the token request is rejected. The symptom looks like a broken sign-in; the cause is key lifetime, and it is worth checking first when a flow that used to work stops working at the redemption step only.

  • What exactly must the authorization server do at the token endpoint when the authorization request carried `dpop_jkt`?
    Compute the RFC 7638 JWK SHA-256 Thumbprint of the public key in the DPoP proof's `jwk` header and compare it with the `dpop_jkt` value recorded against that code. A mismatch MUST be rejected, so the code cannot be redeemed with a different key.
  • Does `dpop_jkt` make the `cnf` claim in the issued access token unnecessary?
    No. `dpop_jkt` binds the authorization code and is sent by the client; `cnf.jkt` binds the issued access token and is minted by the authorization server. The two strings usually match, because they are thumbprints of one key, but they constrain different artefacts at different moments.
  • Is it a problem that `dpop_jkt` is visible in a front-channel query string?
    No. It is a digest of a public key, not a secret, and observing it confers nothing: redeeming the code still requires a signature under the private half. Its confidentiality was never the property being relied on.

saying these in an interview costs you the question

  • Thinks dpop_jkt is sent on the token request rather than the authorization request
  • Says dpop_jkt carries the client's private key or a signature
  • Assumes dpop_jkt binds the access token rather than the authorization code
  • Treats the declared thumbprint as authentication of the client
  • Believes the server may skip the comparison when a valid proof is present