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

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.

  1. Get a signed token pair for the printer

    Recorded as oauth.token succeeded for lab-printer.

  2. Present the genuine access token to UserInfo

    Recorded as oidc.userinfo succeeded (userinfo_served).

  3. See a validly signed token refused for the wrong use

    Recorded as oidc.userinfo rejected (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.

  1. On this lab page choose Lab Photos and press Start.

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

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

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

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

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

  2. A signature does not say who the message is for. Sign in through $ISSUER/token-decoder as 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.

  1. Header values are hints. Look again at the kid and alg in the access token header. btl-lab verify uses the kid only to pick a key from the key set it fetched from the issuer you configured, and accepts alg only 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.

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

  1. A signature does not show freshness. Keep $TOKEN and repeat the first command of step 3 once the token's lifetime has passed (the exp minus iat in 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

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

  2. 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.token succeeded for lab-printer.

  • oidc.userinfo succeeded with reason userinfo_served.

  • oidc.userinfo rejected with reason invalid_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

  1. Delete tokens.json and run unset TOKEN ID_TOKEN DEC_ID_TOKEN OTHER.

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