OAUTH 2.0 · LAB
Build DPoP proof contents and hashes by hand
Reproduce the lesson's thumbprint and ath values exactly, assemble the token endpoint and UserInfo proof headers and claims for lab-printer-app, and catch the small mistakes that never match.
PlannedUses your lab tenant
The lesson
Builds on: Binding a token to a key.
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.
- G14 DPoP
Setup
Keep
app-key.pem,app-key.jwk, theb64urlhelper andAPP_IDfrom Bind the phone app's tokens to a key it generated.Have a current access token for
lab-printer-appinTOKEN. Run that lab's Do today steps 2 and 3 if you need a new one.
Note: the lab toolkit has no command yet that signs a DPoP proof with your own key. Everything up to the signature runs today; sending signed proofs needs that command and G14.
Planned walkthrough
Build and sign the token endpoint proof described in Do today step 3, then redeem a code for
lab-printer-appwith it in theDPoPheader.
curl -s "$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 | jq '{token_type}'
Expect "token_type": "DPoP".
Why it matters: "The proof at the token endpoint". The proof carries no secret, code or body content; it names only the method, the address, the moment and a unique identifier.
Build and sign the UserInfo proof from Do today step 4 for the new token, then call UserInfo with
Authorization: DPoP $TOKENand the proof in theDPoPheader. Ava's claims come back.
Why it matters: "The proof at the photo API". The proof names this token through ath, so it is useless with any other token.
Send the same proof again with a newer token from the same app. Expect
401withWWW-Authenticate: DPoP error="invalid_token", becauseathnames the old token.
Why it matters: the token and the proof are each useless without the other.
Do today
Reproduce the lesson's thumbprint from its public key, by hand and with the toolkit.
printf %s '{"crv":"P-256","kty":"EC","x":"DboN9G_lcWgZ3rxckKu35JwH2OYBLWJgxcT_-KEXK8Y","y":"rj69LkdPmdnAbPoBq6mpcpbJPWLEQ6Qs2SaMMyKWBSk"}' \
| openssl dgst -sha256 -binary | b64url; echo
printf %s '{"kty":"EC","crv":"P-256","x":"DboN9G_lcWgZ3rxckKu35JwH2OYBLWJgxcT_-KEXK8Y","y":"rj69LkdPmdnAbPoBq6mpcpbJPWLEQ6Qs2SaMMyKWBSk"}' > lesson-key.jwk
btl-lab thumbprint lesson-key.jwk
Both print BVohHgHi0D9O5iBFRpVSFAukBjzgVbIRUjYuTsfJER8. The hand-made input is the canonical form: required members only, in alphabetical order, no whitespace.
Reproduce the lesson's
ath.
printf %s demo-app-access-token-1 | openssl dgst -sha256 -binary | b64url; echo
The result is xcbjgyKf_F3-foHYAOd_VdSpNEB0ThBmPo1xXGoYihA. Hash the exact token string, nothing around it.
Assemble the token endpoint proof's header and claims for your own key.
HEADER=$(jq -nc --slurpfile k app-key.jwk '{typ:"dpop+jwt", alg:"ES256", jwk:$k[0]}')
CLAIMS=$(jq -nc --arg u "$ISSUER/oauth/token" --arg j "$(openssl rand 16 | b64url)" --argjson t $(date +%s) '{jti:$j, htm:"POST", htu:$u, iat:$t}')
echo "$HEADER" | jq .; echo "$CLAIMS" | jq .
echo "$(printf %s "$HEADER" | b64url).$(printf %s "$CLAIMS" | b64url)" # the signing input
Check the header against the lesson: typ is dpop+jwt, alg is asymmetric, and jwk holds only kty, crv, x and y. The last line is what the private key signs; the signature is then appended after another dot.
Assemble the UserInfo proof for your real token, and compute its
ath.
ATH=$(printf %s "$TOKEN" | openssl dgst -sha256 -binary | b64url)
jq -nc --arg u "$ISSUER/oidc/userinfo" --arg j "$(openssl rand 16 | b64url)" --argjson t $(date +%s) --arg a "$ATH" \
'{jti:$j, htm:"GET", htu:$u, iat:$t, ath:$a}' | jq .
Every claim differs from step 3, and ath is new. If the request were for $ISSUER/oidc/userinfo?x=1, htu would still be $ISSUER/oidc/userinfo: the query string and fragment are left out.
Build two token endpoint claim sets a second apart and compare them.
jtiandiatdiffer: each request a correct client sends gets its own proof.
Break it
These are the lesson's common mistakes, checked offline today. Once G14 exists, the tenant rejects each one.
Add
kidto the thumbprint input and hash it by hand.
printf %s '{"crv":"P-256","kid":"app-1","kty":"EC","x":"DboN9G_lcWgZ3rxckKu35JwH2OYBLWJgxcT_-KEXK8Y","y":"rj69LkdPmdnAbPoBq6mpcpbJPWLEQ6Qs2SaMMyKWBSk"}' \
| openssl dgst -sha256 -binary | b64url; echo
The value changes and would never match cnf.jkt. Reordering the members or adding spaces does the same.
Hash the whole header value instead of the token:
printf %s "DPoP $TOKEN" | openssl dgst -sha256 -binary | b64url. The result differs fromATH, so a resource would answerinvalid_token.Encode with ordinary Base64 (
openssl dgst -sha256 -binary | base64). The+,/and=characters give a value that looks plausible and never matches.
Check your work
Your step 1 and step 2 outputs equal the lesson's values exactly, and the jq output in steps 3 and 4 shows exactly the claims the lesson lists. Once G14 exists, Audit shows oauth.token succeeded with token type DPoP and oidc.userinfo userinfo_served for lab-printer-app.
Cleanup
Delete lesson-key.jwk. Keep app-key.pem and app-key.jwk for the next DPoP labs.
Missing infrastructure
G14: the tenant must accept and check proofs before sending them has any effect. Today the token endpoint ignores a
DPoPheader and UserInfo refuses the DPoP scheme. Once G14 exists and the toolkit can sign a proof with your own key, the Planned walkthrough runs as written.