OPENID CONNECT · LAB
Read an ID token and an access token side by side, and send each to the wrong reader
Take one token response apart, compare the two tokens' audiences, types and keys, and see each one refused where the other belongs.
ReadyUses your lab tenant
The lesson
Builds on: From delegated access to sign-in.
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.
Sign Ava in to lab-collage asking for sign-in and photo access
Recorded as
oauth.authorizesucceeded (code_issued) forlab-collageabout[email protected].Receive both tokens in one response
Recorded as
oauth.tokensucceeded forlab-collageabout[email protected].Use the access token at the provider's API
Recorded as
oidc.userinfosucceeded (userinfo_served) forlab-collage.
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
You need
lab-collage, Ava and the shell variables from the first lab in this track. Keepbtl-lab callbackrunning in a second terminal.Press Start.
Walkthrough
See both tokens in a browser first. Open
$ISSUER/token-decoder, keep the code flow and sign in as Ava. The decoder shows an ID token and an access token from one response, with their signatures checked against the tenant's keys.
Why it matters: one response, two tokens, and on this tenant both are JWTs. They look alike, which is exactly why they get mixed up.
Run your own
lab-collagesign-in that asks for sign-in and photo access together,scope=openid photos.read:
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid%20photos.read&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Approve as Ava, check state and iss from the listener, and exchange the code:
CODE='<code from the listener>'
RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_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")
TOKEN=$(jq -r .access_token <<<"$RESP"); ID_TOKEN=$(jq -r .id_token <<<"$RESP")
Read the ID token the way the lesson does:
btl-lab decode "$ID_TOKEN".Header:
algisRS256, with akidandtyp: JWT.Claims:
iss,sub,aud(your client ID),azp,exp,iat,auth_time,nonce,at_hashandamr.Work out
exp - iat, the ID token lifetime, and how farauth_timelies beforeiat.
Why it matters: each claim answers one of the lesson's questions: who issued it, about whom, for which client, when, and for which attempt.
Notice what is missing: no name, no email address, no scope. You asked only for
openidand a photo scope.
Why it matters: an ID token reports an authentication event. It is not a profile and not a permission.
Read the access token from the same response:
btl-lab decode "$TOKEN". On a default tenant it hastyp: at+jwt,alg: ES256,audset to$ISSUER/resource, plusclient_idandscope. If your access token manager issues opaque tokens, the decode shows nothing readable, which is the lesson's point that the format is a matter between the provider and its API.
Fill in the lesson's comparison table with the real values:
| ID token | Access token | |
|---|---|---|
Who it is for (aud) | lab-collage client ID | $ISSUER/resource |
Type (typ) | JWT | at+jwt |
| Signing algorithm and key | RS256, ID token key | ES256, access token key |
| Where it goes next | Nowhere | The Authorization header |
Send the access token to its reader. The provider's API here is UserInfo:
GET$ISSUER/oidc/userinfo
Open in console
GET $ISSUER/oidc/userinfo HTTP/1.1
Authorization: Bearer $TOKENThe answer is 200 with Ava's sub.
Validate each token as its own reader would, then as the wrong one:
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE"
btl-lab verify "$TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
btl-lab verify "$TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE"
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
The first two end in ACCEPT. The access token checked as an ID token fails early, because ES256 is not on the ID token algorithm list and its type is wrong. The ID token checked as an access token fails at the type check: JWT is not at+jwt.
Why it matters: each token is acceptable only to its own reader, and the checks that enforce this are the audience and the type.
Break it
Send the ID token to the API as if it were an access token:
curl -si "$ISSUER/oidc/userinfo" -H "Authorization: Bearer $ID_TOKEN" | head -5
Expect 401 with WWW-Authenticate: Bearer error="invalid_token". The tenant accepts only tokens it issued as access tokens.
An ID token is not a session. Wait until
exphas passed (five minutes on a default tenant) and run the firstbtl-lab verifyline again. It now fails the expiry check, yet nobody would expect Ava to be signed out of the collage app at that moment. The application's own session, which you build later in this track, takes over once the ID token has done its job.
Check your work
Press Check my progress. It looks for oauth.authorize with code_issued and oauth.token succeeded for lab-collage and Ava, then oidc.userinfo with userinfo_served for lab-collage.
The ID token sent as a bearer token is not in Audit, because the tenant never issued it as an access token and cannot attribute it. Find it in Logs as oidc.userinfo rejected with invalid_token. The Token Decoder run from step 1 appears in Audit under the built-in Token Decoder client.
Cleanup
Nothing to remove. Unset the tokens when you finish: unset TOKEN ID_TOKEN RESP.