Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

OAUTH 2.0 · LAB

Add freshness, jti tracking and server nonces to DPoP proofs

Turn on DPoP nonces, handle use_dpop_nonce as a client by retrying once with a fresh proof, and see why every retry needs a new jti. Today, see how identical bearer requests look to the tenant.

PlannedUses your lab tenant

The lesson

Builds on: Validating proofs at the server.

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

Planned. The core of this lab waits on platform features that are not built yet. The planned walkthrough shows exactly how it will run; Do today is a real exercise you can do now.

Setup

  1. Keep app-key.pem, app-key.jwk, the b64url helper and the lab-printer-app variables from the earlier DPoP labs.

  2. Planned (G14): in OAuth > Flow policy > DPoP, set proof age to 60 seconds, clock tolerance to 5 seconds, jti tracking on, nonces for Token endpoint and UserInfo, and nonce rotation every 5 minutes. Both stores appear as usage policies in the tenant's usage view.

Note: the lab toolkit has no command yet that signs a DPoP proof with your own key. The planned steps say exactly what each proof contains; they need that command before they can run.

Planned walkthrough

  1. Redeem a code for lab-printer-app with a proof that has no nonce claim.

curl -si "$ISSUER/oauth/token" -H "DPoP: $PROOF" -d grant_type=authorization_code -d code=<code> \
  --data-urlencode redirect_uri=$REDIRECT -d code_verifier=$VERIFIER -d client_id=$APP_ID

Expect 400 {"error":"use_dpop_nonce"} with a DPoP-Nonce header. Save its value as AS_NONCE.

Why it matters: "Server-provided nonces". Nobody can sign a proof containing a value the server has not yet revealed, so proofs cannot be made in advance.

  1. Retry once with a fresh proof: a new jti, a new iat, and "nonce": "$AS_NONCE". Tokens are issued.

Why it matters: the client retries exactly once, with a new proof, never by resending the old one.

  1. Call UserInfo with a proof that carries AS_NONCE. Expect 401 with WWW-Authenticate: DPoP error="use_dpop_nonce" and a different DPoP-Nonce. Retry with that value.

Why it matters: each server's nonce is accepted only by that server, so the client keeps one per server and never swaps them.

  1. Keep calling UserInfo. When a successful response carries a new DPoP-Nonce, switch to it for the next proof.

Why it matters: servers can rotate nonces on successful responses without costing the client a rejected request.

  1. Turn nonces off and keep jti tracking on. Send the same complete UserInfo request, same token and same proof, twice. The first is served; the second is refused.

Why it matters: "What a captured proof allows". A copied token and proof pair can be replayed until the time window closes, unless the server remembers jti values. That is also why a retry must always build a new proof, even for a safe GET.

Do today

  1. Get a fresh lab-printer-app token (see Bind the phone app's tokens to a key it generated, Do today step 2). Then send the identical UserInfo request five times.

for i in 1 2 3 4 5; do curl -s -o /dev/null -w '%{http_code}\n' "$ISSUER/oidc/userinfo" -H "Authorization: Bearer $TOKEN"; done

All five return 200, and Audit shows five oidc.userinfo succeeded userinfo_served events. Nothing distinguishes a repeat from a new request: this is the gap the lesson's time window, jti tracking and nonces close.

  1. Assemble two UserInfo proof claim sets a second apart, as in Build DPoP proof contents and hashes by hand, Do today step 4. Compare jti and iat: a correct client gives every request its own.

  2. Work out the window a pre-made proof would have. Decode your token with btl-lab decode "$TOKEN" and subtract iat from exp. Because ath ties a proof to one token, proofs made in advance are worth at most that long, which is why the lesson ties long-lived bound tokens to nonces.

  3. For "What DPoP does not protect", write down two things a proof does not cover in your UserInfo call: the other headers and any request body. Note which layer protects them in transit.

Break it

Once G14 exists, simulate a client with a wrong clock by building a UserInfo proof whose iat is 120 seconds in the past. The tenant refuses it as stale. Nonces make freshness independent of the client's clock, because the server judges it from its own value.

Check your work

Today, Audit shows five oidc.userinfo succeeded userinfo_served events for lab-printer-app. Once G14 exists, Audit shows oauth.token refused with use_dpop_nonce followed by succeeded, an oidc.userinfo refusal of the replayed proof, and a refusal of the stale one.

Cleanup

  1. Revoke the token: curl -s "$ISSUER/oauth/revoke" -d token=$TOKEN -d client_id=$APP_ID.

  2. Once G14 exists, set the DPoP settings back to the values from the first DPoP lab.

  3. When you finish the DPoP labs, delete app-key.pem and app-key.jwk.

Missing infrastructure

  • G14: nonce issuance and validation, jti tracking with bounded storage, and the proof age policy. The nonce and jti stores should be registered usage policies the tenant can see.

  • G3: a sample resource that issues its own nonces, so the lab can show a resource-side nonce beyond UserInfo.

  • Once these exist and the toolkit can sign proofs, the Planned walkthrough runs as written.

Back to all labs

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

The Lab