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

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.

  1. Sign Ava in to lab-collage asking for sign-in and photo access

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

  2. Receive both tokens in one response

    Recorded as oauth.token succeeded for lab-collage about [email protected].

  3. Use the access token at the provider's API

    Recorded as oidc.userinfo succeeded (userinfo_served) for lab-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

  1. You need lab-collage, Ava and the shell variables from the first lab in this track. Keep btl-lab callback running in a second terminal.

  2. Press Start.

Walkthrough

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

  1. Run your own lab-collage sign-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")
  1. Read the ID token the way the lesson does: btl-lab decode "$ID_TOKEN".

    • Header: alg is RS256, with a kid and typ: JWT.

    • Claims: iss, sub, aud (your client ID), azp, exp, iat, auth_time, nonce, at_hash and amr.

    • Work out exp - iat, the ID token lifetime, and how far auth_time lies before iat.

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.

  1. Notice what is missing: no name, no email address, no scope. You asked only for openid and a photo scope.

Why it matters: an ID token reports an authentication event. It is not a profile and not a permission.

  1. Read the access token from the same response: btl-lab decode "$TOKEN". On a default tenant it has typ: at+jwt, alg: ES256, aud set to $ISSUER/resource, plus client_id and scope. 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.

  1. Fill in the lesson's comparison table with the real values:

ID tokenAccess token
Who it is for (aud)lab-collage client ID$ISSUER/resource
Type (typ)JWTat+jwt
Signing algorithm and keyRS256, ID token keyES256, access token key
Where it goes nextNowhereThe Authorization header
  1. 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 $TOKEN

The answer is 200 with Ava's sub.

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

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

  1. An ID token is not a session. Wait until exp has passed (five minutes on a default tenant) and run the first btl-lab verify line 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.

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