IDENTITY AND TRUST · LAB
Verify a signed token the way the clinic would
Verify real tokens from your tenant step by step, see that a signature covers exact bytes, and reject genuine, validly signed tokens that are meant for another recipient, of the wrong type or expired.
ReadyUses your lab tenant
The lesson
Builds on: Giving another application limited access.
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.
Get a signed token pair for the printer
Recorded as
oauth.tokensucceeded forlab-printer.Present the genuine access token to UserInfo
Recorded as
oidc.userinfosucceeded (userinfo_served).See a validly signed token refused for the wrong use
Recorded as
oidc.userinforejected (invalid_token).
Setup
In the lesson the clinic verifies a lab result signed by the lab. Here your tenant is the signer and you are the verifier, using the toolkit's btl-lab verify. Every token that fails a check below is a real token your tenant issued; nothing is altered.
On this lab page choose Lab Photos and press Start.
Set the variables and helpers from the earlier labs:
ISSUER=https://tenant-<id>.beyondthelogin.dev
CLIENT_ID=<lab-printer client ID>
read -rs CLIENT_SECRET
REDIRECT=http://127.0.0.1:8765/callback
authz() { echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(node -p 'encodeURIComponent(process.argv[1])' "$REDIRECT")&scope=$1&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"; }
redeem() { curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=authorization_code --data-urlencode "code=$CODE" --data-urlencode "redirect_uri=$REDIRECT" -d code_verifier="$VERIFIER" "$ISSUER/oauth/token"; }
fresh() { eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; }
Walkthrough
Get a token pair. Run
fresh; authz openid%20profile, open the URL, sign in as Ava, and redeem:
CODE=<code from the callback>
redeem > tokens.json
TOKEN=$(jq -r .access_token tokens.json); ID_TOKEN=$(jq -r .id_token tokens.json)
btl-lab decode "$TOKEN"; btl-lab decode "$ID_TOKEN"
Expected: the access token header has "alg":"ES256" and "typ":"at+jwt"; the ID token header has "alg":"RS256" and a different kid.
Why it matters: the lesson's example uses ES256, ECDSA with the P-256 curve and SHA-256. Your tenant signs access tokens that way and ID tokens with RSA, each with its own key.
A signature covers exact bytes. A JWS signs the first two segments exactly as they appear. Hash them, then hash two strings one character apart:
printf %s "$(cut -d. -f1,2 <<<"$ID_TOKEN")" | openssl dgst -sha256
printf %s 'The same text with one character changed' | openssl dgst -sha256
printf %s 'The same text with one character changeD' | openssl dgst -sha256
The last two hashes have nothing in common.
Why it matters: the verifier checks the encoded characters as received and never re-serializes the JSON first. Reformatting a signed message, even only its spacing, breaks the signature.
Verify both tokens, then use the access token:
btl-lab verify "$TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE"
curl -s -H "Authorization: Bearer $TOKEN" "$ISSUER/oidc/userinfo" | jq .sub
Expected: every check passes for both, and UserInfo returns Ava's sub.
Why it matters: the pattern is always the same. Find the trusted key, verify the signature over the exact bytes, then check that the content fits this use.
A signature does not hide anything. The decoded ID token in step 1 shows Ava's name. Anyone holding the token can read it, so never paste real tokens into third-party decoding sites.
A signature does not say who the message is for. Sign in through
$ISSUER/token-decoderas Ava, copy its ID token, and verify it as if it were meant for the printer:
read -rs DEC_ID_TOKEN
btl-lab verify "$DEC_ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id
Expected: the signature passes and the audience check fails.
Why it matters: a genuine message for another recipient is still the wrong message here. That is why the lab result in the lesson names its audience inside the signed content.
Header values are hints. Look again at the
kidandalgin the access token header.btl-lab verifyuses thekidonly to pick a key from the key set it fetched from the issuer you configured, and acceptsalgonly from its own allowlist. If you set up Lab Mail in the Trust across systems lab, verify a Lab Mail access token against your issuer:
read -rs OTHER
btl-lab verify "$OTHER" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
Expected: the key is not found in your issuer's set and iss does not match, although that token is correctly signed by its own issuer.
Why it matters: the verifier, not the message, decides which issuers, keys and algorithms are acceptable.
Type confusion:
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
curl -si -H "Authorization: Bearer $ID_TOKEN" "$ISSUER/oidc/userinfo" | grep -iE '^(HTTP|www-authenticate)'
Expected: the signature passes while typ and aud fail, and UserInfo returns 401 with error="invalid_token".
Why it matters: the type check keeps one kind of signed message from being mistaken for another from the same issuer, as typ does for the lesson's lab result.
A signature does not show freshness. Keep
$TOKENand repeat the first command of step 3 once the token's lifetime has passed (theexpminusiatin the decoded payload). The time check fails while the signature still verifies, and UserInfo refuses it. If you do not want to wait, use the 60 second manager technique from the Protecting credentials and messages lab.
Break it
Present each genuine token where it does not belong: the ID token as an access token (step 7) and the Token Decoder's ID token to the printer (step 5). Each fails on a different check while its signature stays valid.
After the Storing and using keys lab, verify a token signed by the key you disabled there. The key is no longer in the issuer's set, so the token is refused.
Check your work
Press Check my progress. It looks for:
oauth.tokensucceeded forlab-printer.oidc.userinfosucceeded with reasonuserinfo_served.oidc.userinforejected with reasoninvalid_token.
Your terminal should show a passing verification, an audience failure, a type failure, an expired token and, with Lab Mail, an unknown key with an issuer mismatch.
Cleanup
Delete
tokens.jsonand rununset TOKEN ID_TOKEN DEC_ID_TOKEN OTHER.