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

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.

  1. The printer receives Ava's tokens

    Recorded as oauth.token succeeded for lab-printer.

  2. Ask UserInfo who the printer's token belongs to

    Recorded as oidc.userinfo succeeded (userinfo_served) for lab-printer.

  3. Sign Ava in to the collage app with OpenID Connect

    Recorded as oauth.token succeeded for lab-collage.

  4. The collage app cannot revoke the printer's token

    Recorded as oauth.revoke rejected (other_client_token) for lab-collage.

  5. The printer revokes its own token

    Recorded as oauth.revoke succeeded (access_token_found) for lab-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.

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

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

  3. In OAuth > Clients, create lab-collage from the Web preset: confidential, PKCE required, consent mode remember. Add both lab redirect URIs, http://127.0.0.1:8765/callback and https://beyondthelogin.dev/lab/callback/. The client secret is shown once.

  4. Create lab-printer the same way if an OAuth lab has not already created it.

  5. 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
  1. In a second terminal, keep the loopback listener ready for each sign-in in this track: btl-lab callback. It prints code, state and iss, then exits. If you have no terminal handy, the hosted callback page at https://beyondthelogin.dev/lab/callback/ shows the same values.

  2. Press Start on this page.

Walkthrough

  1. The printer connects to Ava's photos. Create fresh request values and build the authorization request for lab-printer. It asks for openid too, 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.

  1. The /me shortcut. Ask the provider who the token belongs to. UserInfo plays the role of the lesson's /me endpoint.

GET$ISSUER/oidc/userinfo Open in console
GET $ISSUER/oidc/userinfo HTTP/1.1
Authorization: Bearer $PRINTER_AT

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

  1. Now play the collage app's backend. Imagine the collage app received $PRINTER_AT from 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.

  1. The collage app signs Ava in with OpenID Connect. Run a fresh request for lab-collage with openid 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.

  1. Read what the ID token says: btl-lab decode "$ID_TOKEN". Note iss, sub, aud, azp, nonce and auth_time. The aud and azp values are the lab-collage client ID, and nonce is $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.

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

  1. Time. Compare iat and auth_time in $ID_TOKEN. Wait a few minutes, run step 4 again without signing in (the tenant session is still valid), and decode the new token. iat moved, auth_time did 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

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

  1. The printer revokes its own token, then the /me call 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.token succeeded for lab-printer, then oidc.userinfo succeeded with userinfo_served, attributed to lab-printer even when the collage side sent the token.

  • oauth.token succeeded for lab-collage.

  • oauth.revoke rejected with other_client_token for lab-collage, then succeeded with access_token_found for lab-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.

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