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

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.

  1. Receive an ID token and access token from the authorization endpoint

    Recorded as oauth.authorize succeeded (tokens_issued) for lab-collage about [email protected].

  2. Redeem the hybrid flow's code

    Recorded as oauth.token succeeded for lab-collage.

  3. Remove the legacy response types from lab-collage

    Recorded as tenant.oauth.clients.update succeeded.

  4. See the tenant refuse the removed implicit type

    Recorded as oauth.authorize rejected (unauthorized_client) for lab-collage.

Setup

Implicit and hybrid responses, at_hash and c_hash, and per-client removal of response types are all real.

  1. Keep the Flow policy and lab-collage settings from Deliver one sign-in by query, fragment and form post: the implicit grant, all six response types, and all three modes.

  2. 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 '='; }
  1. Run btl-lab callback before each request and press Start.

Walkthrough

  1. 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.

  1. 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.

  1. id_token alone:

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.

  1. 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.

  1. Redeem the code and validate the second ID token in full, the nonce again included. Its iss and sub must equal the first's, and it may lack c_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.

  1. Migrate. On lab-collage, remove every response type except code, and remove the implicit grant. 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

  1. A code from another attempt. Allow code id_token and the implicit grant on lab-collage again for a moment, run walkthrough step 4 twice, and compare the first run's ID token c_hash with the second run's code: mismatch, so your handler refuses before redeeming. That is the substitution check doing its job with real values.

  2. 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 two tr commands

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.

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