OPENID CONNECT · LAB
Turn an access token into a false sign-in, then see the ID token refuse it
Give the collage app a genuine access token issued to the printer, watch UserInfo answer anyway, and see how an ID token names its client and attempt.
ReadyUses your lab tenant
The lesson
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.
The printer receives Ava's tokens
Recorded as
oauth.tokensucceeded forlab-printer.Ask UserInfo who the printer's token belongs to
Recorded as
oidc.userinfosucceeded (userinfo_served) forlab-printer.Sign Ava in to the collage app with OpenID Connect
Recorded as
oauth.tokensucceeded forlab-collage.The collage app cannot revoke the printer's token
Recorded as
oauth.revokerejected (other_client_token) forlab-collage.The printer revokes its own token
Recorded as
oauth.revokesucceeded (access_token_found) forlab-printer.
Request console
Requests in this lab can be sent from this page to your tenant: open one and choose Send. Fill in the values below first. They stay in this page's memory and are gone when you leave; secrets are never stored or sent anywhere except the request you send.
Setup
This lab sets up what the whole OpenID Connect track uses: the lab tenant, Ava, and two web clients.
Use your Lab Photos tenant. If it does not have Ava, Ben and the lab scopes yet, reset the lab tenant with the Lab Photos preset (Overview > Reset tenant). A reset clears clients from earlier labs.
In Users, set a password for Ava Archer (
[email protected]) and Ben Okafor ([email protected]). You type them only on the tenant's sign-in page.In OAuth > Clients, create
lab-collagefrom the Web preset: confidential, PKCE required, consent mode remember. Add both lab redirect URIs,http://127.0.0.1:8765/callbackandhttps://beyondthelogin.dev/lab/callback/. The client secret is shown once.Create
lab-printerthe same way if an OAuth lab has not already created it.Store the values in your shell. Secrets are typed, never pasted into a command line or a file.
export ISSUER='https://<your Lab Photos issuer from Overview>'
export CLIENT_ID='<lab-collage client ID>'; read -rs CLIENT_SECRET; export CLIENT_SECRET
export PRINTER_ID='<lab-printer client ID>'; read -rs PRINTER_SECRET; export PRINTER_SECRET
btl-lab env
In a second terminal, keep the loopback listener ready for each sign-in in this track:
btl-lab callback. It printscode,stateandiss, then exits. If you have no terminal handy, the hosted callback page athttps://beyondthelogin.dev/lab/callback/shows the same values.Press Start on this page.
Walkthrough
The printer connects to Ava's photos. Create fresh request values and build the authorization request for
lab-printer. It asks foropenidtoo, because UserInfo only answers tokens that carry it.
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$PRINTER_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid%20profile%20photos.read&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Open the URL in a private window, sign in as Ava and allow access. Confirm the listener printed your $STATE and an iss equal to $ISSUER, then exchange the code:
CODE='<code from the listener>'
RESP=$(curl -s -u "$PRINTER_ID:$PRINTER_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "code=$CODE" --data-urlencode "redirect_uri=http://127.0.0.1:8765/callback" --data-urlencode "code_verifier=$VERIFIER")
PRINTER_AT=$(jq -r .access_token <<<"$RESP"); PRINTER_IDT=$(jq -r .id_token <<<"$RESP")
jq '{token_type, expires_in, scope}' <<<"$RESP"
Why it matters: the printer now legitimately holds an access token for Ava, which is exactly the starting point of the lesson's shortcut.
The
/meshortcut. Ask the provider who the token belongs to. UserInfo plays the role of the lesson's/meendpoint.
GET$ISSUER/oidc/userinfo
Open in console
GET $ISSUER/oidc/userinfo HTTP/1.1
Authorization: Bearer $PRINTER_ATThe answer is 200 with Ava's sub and name. Nothing in it says which client the token was issued to or when Ava last authenticated.
Now play the collage app's backend. Imagine the collage app received
$PRINTER_ATfrom somewhere other than its own token request, a copied token or a phone app that forwards whatever it holds. Send the same request again from the collage side:curl -s "$ISSUER/oidc/userinfo" -H "Authorization: Bearer $PRINTER_AT" | jq .. The answer is identical.
Why it matters: the token is genuine and the answer is correct. A collage backend that signed in "whoever UserInfo names" would sign this person in as Ava, although the token was never meant for the collage app. This is the access token injection the lesson describes.
The collage app signs Ava in with OpenID Connect. Run a fresh request for
lab-collagewithopenid profile. Ava is already signed in at the tenant, so after consent you return at once.
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; COLLAGE_NONCE=$NONCE
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid%20profile&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Check state and iss from the listener, then exchange the code with -u "$CLIENT_ID:$CLIENT_SECRET" exactly as in step 1 and keep TOKEN and ID_TOKEN from the response.
Read what the ID token says:
btl-lab decode "$ID_TOKEN". Noteiss,sub,aud,azp,nonceandauth_time. Theaudandazpvalues are thelab-collageclient ID, andnonceis$COLLAGE_NONCE. The tool reminds you that decoding is not validating.
Why it matters: the ID token is addressed to one client and bound to one attempt, the two facts the access token never carried.
Let the collage app validate both ID tokens it could be handed.
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$COLLAGE_NONCE"
btl-lab verify "$PRINTER_IDT" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$COLLAGE_NONCE"
The first run ends in ACCEPT. The second, a real ID token the same tenant issued to the printer, passes the signature and issuer checks and stops at the audience check.
Why it matters: a token issued to another client names that client, so the collage app refuses it. The audience check is what the /me shortcut was missing.
Time. Compare
iatandauth_timein$ID_TOKEN. Wait a few minutes, run step 4 again without signing in (the tenant session is still valid), and decode the new token.iatmoved,auth_timedid not.
Why it matters: the lesson's second problem. Holding a token shows that an authorization exists. Only auth_time says when someone actually authenticated.
Break it
The collage backend tries to end the printer's grant with its own credentials:
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" -d "token=$PRINTER_AT" | jq .
The tenant answers 400 with unauthorized_client. A client can revoke only its own tokens.
The printer revokes its own token, then the
/mecall is repeated:
curl -s -u "$PRINTER_ID:$PRINTER_SECRET" "$ISSUER/oauth/revoke" -d "token=$PRINTER_AT"
curl -si "$ISSUER/oidc/userinfo" -H "Authorization: Bearer $PRINTER_AT" | head -5
UserInfo now returns 401 with WWW-Authenticate: Bearer error="invalid_token". A relying party that built its sign-in on UserInfo depends on another client's token lifecycle.
Check your work
Press Check my progress. The checks follow the walkthrough:
oauth.tokensucceeded forlab-printer, thenoidc.userinfosucceeded withuserinfo_served, attributed tolab-printereven when the collage side sent the token.oauth.tokensucceeded forlab-collage.oauth.revokerejected withother_client_tokenforlab-collage, then succeeded withaccess_token_foundforlab-printer.
The final UserInfo call with the revoked token is not in Audit, because the tenant no longer knows the token. Find it in Logs as oidc.userinfo rejected with invalid_token. Your own verifier's audience refusal in step 6 appears nowhere at the tenant: relying-party validation happens only at the relying party.
Cleanup
Keep Ava, Ben, lab-collage, lab-printer and your shell variables. The rest of the track uses them. Unset the copied tokens with unset PRINTER_AT PRINTER_IDT RESP.