OPENID CONNECT · LAB
Produce the four kinds of validation failure and read them from your logs
Feed lab-collage real tokens that are wrong for it and real configuration mistakes, diagnose each from a structured rejection log, and see why each tempting quick fix lets the wrong token in.
Partly readyIncludes a simulationUses both lab tenants
The lesson
Builds on: Signing keys and rotation.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.
- G66 Second lab tenant for every learner: additional tenants need a paid subscription or a BTL grant, so labs that use Lab Mail cannot be completed by an ordinary learner yet
Needs a second tenant. This lab also uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so you may not be able to do the Lab Mail steps yet (gap G66).
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.
Obtain a genuine ID token issued to the printer
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].Move lab-collage's ID tokens to an ES256 key
Recorded as
tenant.oauth.id_token_managers.updatesucceeded.Move them back to the RS256 key
Recorded as
tenant.oauth.id_token_managers.updatesucceeded.Shorten the ID token lifetime to 60 seconds
Recorded as
tenant.oauth.id_token_managers.updatesucceeded.Receive a token you will let expire
Recorded as
oauth.tokensucceeded forlab-collageabout[email protected].Restore the 300-second lifetime
Recorded as
tenant.oauth.id_token_managers.updatesucceeded.
Setup
This lab adds the second lab tenant, which the Discovery and Claims labs reuse.
Create a second tenant from the tenant switcher and name it
Lab Mail. Additional tenants currently need a paid subscription or a BTL grant (G66). It plays a different provider, the lesson's "another provider".In
Lab Mail, create the user[email protected](Ava Archer) with a password, and the clientlab-mail-collagefrom the Web preset with the redirect URIhttps://beyondthelogin.dev/lab/callback/. Store its values:
export ISSUER2='https://<your Lab Mail issuer from Overview>'
export MAIL_CLIENT_ID='<lab-mail-collage client ID>'; read -rs MAIL_CLIENT_SECRET; export MAIL_CLIENT_SECRET
You also need
lab-collage(ID token manager on key B),lab-printer, Ava in Lab Photos, the helpers from the authentication request lab,EXPECTED_ISSUER,EXPECTED_AUD,ID_ALGSandJWKS_CACHE. Keepbtl-lab callbackrunning.Send every rejection to a structured log line, in the format the lesson describes:
export REJECTIONS="$HOME/oidc-rejections.jsonl"
vcheck() { btl-lab verify "$1" --issuer "$EXPECTED_ISSUER" --audience "${AUD-$EXPECTED_AUD}" --algs "${ALGS:-$ID_ALGS}" --type id --nonce "$2" --jwks-cache "$JWKS_CACHE" --log "$REJECTIONS" "${@:3}"; }
In Lab Photos, press Start.
Walkthrough
Attack-shaped: a token from a provider you never configured. Sign in to
Lab Mailthroughlab-mail-collage:
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; MAIL_NONCE=$NONCE
echo "$ISSUER2/oauth/authorize?response_type=code&client_id=$MAIL_CLIENT_ID&redirect_uri=https%3A%2F%2Fbeyondthelogin.dev%2Flab%2Fcallback%2F&scope=openid&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Sign in as the Lab Mail Ava. The hosted callback page shows code, state and iss. Redeem the code at Lab Mail and keep the ID token:
MAIL_IDT=$(curl -s -u "$MAIL_CLIENT_ID:$MAIL_CLIENT_SECRET" "$ISSUER2/oauth/token" -d grant_type=authorization_code --data-urlencode "code=<code>" --data-urlencode "redirect_uri=https://beyondthelogin.dev/lab/callback/" --data-urlencode "code_verifier=$VERIFIER" | jq -r .id_token)
vcheck "$MAIL_IDT" "$MAIL_NONCE"
The verifier fetches your Lab Photos key set once, finds no key with that kid, and stops at the key check.
Why it matters: a genuine token from a provider you never configured fails before any claim is read.
The same token under a configuration mistake: a key set address from the wrong provider.
btl-lab verify "$MAIL_IDT" --issuer "$EXPECTED_ISSUER" --audience "$EXPECTED_AUD" --algs "$ID_ALGS" --type id --nonce "$MAIL_NONCE" --jwks-uri "$ISSUER2/oauth/jwks" --log "$REJECTIONS"
Now the signature passes, and the issuer check refuses it.
Why it matters: a verified signature identifies the issuer only when the key came from the expected issuer's own set. The exact iss comparison is the backstop.
A genuine token issued to another client, and a token from another attempt. Run a
lab-printersign-in withscope=openidin Lab Photos, keepPRINTER_IDTandPRINTER_NONCE, then runvcheck "$PRINTER_IDT" "$PRINTER_NONCE": it stops at the audience check. Run a freshlab-collagesign-in and check its token against an older attempt's nonce:vcheck "$ID_TOKEN" "$PRINTER_NONCE"stops at the nonce check.
Why it matters: these are the scattered failures at the audience and nonce checks that the lesson asks you to treat as possible attacks, even when rare.
Configuration mismatch, the lesson's Tuesday release. The upgraded library lost its expected audience. Reproduce it on three fresh, valid
lab-collagetokens:
AUD= vcheck "$ID_TOKEN" "$NONCE"
Every run stops at the audience check, and each log line shows "expected_audience": null.
Why it matters: every sign-in failing at the same check, starting right after a change, points at your own configuration, and the expected values in the log are the whole answer.
Configuration mismatch at the provider. Edit the
lab-collageID token manager to sign with ES256 key D. Sign in and runvcheck "$ID_TOKEN" "$NONCE": it stops at the algorithm check. The discovery document now listsES256inid_token_signing_alg_values_supported. Decide the fix on purpose: either both sides move to ES256 together, or the provider goes back.
Restore: edit the lab-collage ID token manager to sign with key B again.
Expiry. Set the
lab-collageID token manager's lifetime to 60 seconds, sign in, wait two minutes, and runvcheck "$ID_TOKEN" "$NONCE". It stops at the expiry check, and the log shows atoken_age_secondsof about 120.
Restore: set the lifetime back to 300 seconds.
Clock drift.
Simulation. you cannot change the tenant's clock, so you shift your verifier's clock instead. The token is exactly as the tenant issued it.
Run vcheck "$ID_TOKEN" "$NONCE" --clock-offset -600 on a fresh token. It stops at the issued-at check with a negative token_age_seconds. Without the option the same token passes. In production that pattern follows one server.
Key set unavailable. Start with no cached set and a key set address that does not exist on the tenant:
rm -f "$JWKS_CACHE"
btl-lab verify "$ID_TOKEN" --issuer "$EXPECTED_ISSUER" --audience "$EXPECTED_AUD" --algs "$ID_ALGS" --type id --nonce "$NONCE" --jwks-cache "$JWKS_CACHE" --jwks-uri "$ISSUER/oauth/jwks-missing" --log "$REJECTIONS"
The run stops because the key set is unavailable. Now run vcheck "$ID_TOKEN" "$NONCE" to fill the cache, and repeat the command above. It accepts, because the kid is already in a set you fetched recently.
Why it matters: this is the decision the lesson asks you to make in advance. Keep validating with a recent copy when the kid is known, and fail when you cannot verify.
Read the pattern from your logs:
jq -s 'group_by(.check) | map({check: .[0].check, count: length, expected_audience: (map(.expected_audience) | unique)})' "$REJECTIONS"
Confirm that no line holds a token, a code, a nonce, a name or an email address.
What the person sees. Write the page for every one of these failures: one message, a "try again" link that starts a fresh attempt, and the reference (your correlation ID). It never says which check failed and never shows a name from the rejected token.
Compare with the provider's view. Lab Photos Audit shows
oauth.tokensucceeded for every sign-in you rejected. Relying-party validation failures exist only in the relying party's own logs.
Break it
Each tempting fix in the lesson's table makes your relying party accept a real token it should refuse. Try each once, then remove it:
Trust any audience:
vcheck "$PRINTER_IDT" "$PRINTER_NONCE" --trusted-audience "$PRINTER_ID". The printer's token now passes. Fix it instead by setting the expected audience correctly.
Restore: run vcheck without --trusted-audience from now on.
Widen the clock tolerance: run the expired token from step 6 with
--leeway 3600 --max-iat-age 3600. It passes the time checks. Fix the clock instead.
Restore: use the default leeway and issued-at window again.
Accept whatever the provider lists:
ALGS=RS256,ES256would have hidden the algorithm change in step 5. Change the registration and the allowlist together, on purpose.
Restore: run unset ALGS, so ID_ALGS (RS256) applies again.
Check your work
Press Check my progress in Lab Photos. It looks for, in order: oauth.token succeeded for lab-printer, the move of the lab-collage ID token manager to key D and back, the lifetime change to 60 seconds, a lab-collage token request, and the lifetime change back to 300 seconds.
$REJECTIONS should hold at least one line each for the key, issuer, audience, nonce, algorithm, expiry, issued-at and key set unavailable checks. In Lab Mail, Audit shows oauth.token succeeded for lab-mail-collage.
Cleanup
Confirm that the
lab-collageID token manager signs with key B and has a 300-second lifetime.Keep
Lab Mail,ISSUER2,MAIL_CLIENT_ID,MAIL_CLIENT_SECRETand$REJECTIONSfor the Discovery labs. Rununset PRINTER_IDT PRINTER_NONCE AUD ALGS.
Missing infrastructure
G66 Second lab tenant: this lab uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so an ordinary learner can do only the Lab Photos steps until every learner can have a second lab tenant.