OPENID CONNECT · LAB
Verify at_hash and c_hash, then migrate to code
Validate an implicit response with at_hash and a hybrid form-post response with c_hash and a second ID token, then remove the legacy response types and watch the tenant refuse them.
ReadyUses your lab tenant
The lesson
Builds on: Response types and response modes.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Your progress
Press Start before you begin. Only events your tenant records after that count, in the order below. Checking reads your tenant's Audit, so you need Audit read access in it.
Sign in to start this lab and check your progress. Log in or create an account.
Receive an ID token and access token from the authorization endpoint
Recorded as
oauth.authorizesucceeded (tokens_issued) forlab-collageabout[email protected].Redeem the hybrid flow's code
Recorded as
oauth.tokensucceeded forlab-collage.Remove the legacy response types from lab-collage
Recorded as
tenant.oauth.clients.updatesucceeded.See the tenant refuse the removed implicit type
Recorded as
oauth.authorizerejected (unauthorized_client) forlab-collage.
Setup
Implicit and hybrid responses, at_hash and c_hash, and per-client removal of response types are all real.
Keep the Flow policy and
lab-collagesettings from Deliver one sign-in by query, fragment and form post: theimplicitgrant, all six response types, and all three modes.Add the hash helper. The ID tokens are RS256, so the hash is SHA-256, its left-most 16 bytes, encoded as Base64url without padding:
source ~/btl-oidc.sh
half_hash() { printf %s "$1" | openssl dgst -sha256 -binary | head -c 16 | openssl base64 -A | tr '+/' '-_' | tr -d '='; }
Run
btl-lab callbackbefore each request and press Start.
Walkthrough
The implicit flow.
RT="id_token token" signin
The tokens arrive in the address bar's fragment, and they stay in your history. Copy them into the shell, then validate the ID token, signature and nonce included:
AT='<access_token from the address bar>'; IDT='<id_token from the address bar>'
btl-lab verify "$IDT" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE"
Why it matters: with no code exchange, the ID token's signature and nonce are the only evidence that this response is genuine and belongs to this attempt.
Tie the access token to the ID token:
[ "$(half_hash "$AT")" = "$(part "$IDT" | jq -r .at_hash)" ] && echo "at_hash ok" || echo "at_hash mismatch"
Why it matters: someone who could alter the fragment could swap in another account's access token. at_hash lets you notice.
id_tokenalone:
RT="id_token" signin
No access token, and the ID token has no at_hash. Read its claims with part and note which profile claims, if any, it carries.
Why it matters: when no access token is issued, the ID token is the only carrier, and any profile it holds is traveling in a URL.
The hybrid flow with a form post:
RT="code id_token" signin response_mode=form_post
The listener's POST body has code, id_token, state and iss. Before redeeming anything, validate the first ID token, compare its iss with the iss field, and check c_hash:
CODE='<code>'; IDT1='<id_token>'
btl-lab verify "$IDT1" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE"
[ "$(half_hash "$CODE")" = "$(part "$IDT1" | jq -r .c_hash)" ] && echo "c_hash ok" || echo "c_hash mismatch: do not redeem"
Why it matters: before redeeming the code, you know who signed in and that this code was issued with that statement.
Redeem the code and validate the second ID token in full, the nonce again included. Its
issandsubmust equal the first's, and it may lackc_hash. Use nothing until this passes.
redeem "$CODE"
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE"
diff <(part "$IDT1" | jq -S '{iss, sub}') <(part "$ID_TOKEN" | jq -S '{iss, sub}') && echo "same person"
Why it matters: current guidance checks the nonce in the second token too, so an injected code is caught even if the first token passed.
Migrate. On
lab-collage, remove every response type exceptcode, and remove theimplicitgrant. Then try the old request:
RT="id_token token" signin
error=unauthorized_client.
Why it matters: while old types stay allowed, anyone can start a request that returns tokens to the old callback page.
Break it
A code from another attempt. Allow
code id_tokenand theimplicitgrant onlab-collageagain for a moment, run walkthrough step 4 twice, and compare the first run's ID tokenc_hashwith the second run's code: mismatch, so your handler refuses before redeeming. That is the substitution check doing its job with real values.The common hash mistakes, each producing a mismatch on a genuine response:
hashing all 32 bytes:
printf %s "$CODE" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '='standard Base64 with
+,/and padding: leave out the twotrcommands
Restore: remove code id_token and the implicit grant from lab-collage again, and keep half_hash exactly as defined in Setup. Turning the check off on a mismatch removes the protection these flows depend on.
Check your work
Press Check my progress. The checks look for the implicit response, the hybrid flow's code exchange, the removal of the legacy types from lab-collage, and the tenant's refusal of the old request.
Cleanup
Restore Flow policy to the values you noted in the previous lab: for a tenant set up in these labs, the authorization_code and refresh_token grants, response type code, and modes query and form_post.