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

OAUTH 2.0 · LAB

Validate a JWT access token in order and follow a key rotation

Decode a real at+jwt, validate it check by check, refuse an ID token and another tenant's token, then rotate the signing key and watch a local API keep up.

Partly readyUses both lab tenants

The lesson

Builds on: Using access tokens.

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.

Needs a second tenant. This lab also uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so you may not be able to do the Lab Mail steps yet (gap G66).

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 JWT access token for Ava

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

  2. Publish a new ES256 signing key

    Recorded as tenant.oauth.keys.generate succeeded.

  3. Move Default access tokens to the new key

    Recorded as tenant.oauth.managers.update succeeded.

  4. Get a token signed with the new key

    Recorded as oauth.token succeeded for lab-printer.

  5. Withdraw the old key

    Recorded as tenant.oauth.keys.disable succeeded.

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. Choose Lab Photos as the lab tenant and press Start.

  2. Use the variables and the authorize and exchange helpers from Present an access token correctly, with CLIENT_ID and CLIENT_SECRET for lab-printer. Change exchange to keep the ID token too by adding ID_TOKEN=$(jq -r '.id_token // empty' <<<"$RESP").

  3. Confirm lab-printer uses Default access tokens (Signed JWT).

  4. In Lab Mail, confirm lab-mail-collage exists (Confidential, redirect URI http://127.0.0.1:8765/callback) and that a user you can sign in as exists there, for example [email protected] with her Lab Mail password. Set ISSUER2, MAIL_CLIENT_ID and read -rs MAIL_CLIENT_SECRET.

  5. Start the local photo API in its own terminal: btl-lab resource --mode jwt. It trusts only $ISSUER, expects audience $ISSUER/resource, accepts only ES256, fetches the key set address from the issuer's metadata, and logs one decision per request without the token.

Walkthrough

  1. Get a token: authorize "openid profile photos.read", sign in as Ava, exchange. Then read it and measure it.

btl-lab decode "$TOKEN"; echo "length: ${#TOKEN}"

The header shows alg ES256, a kid and typ at+jwt. The claims show iss, sub, aud ($ISSUER/resource), client_id, scope, iat, exp and jti. The toolkit says plainly that decoding is not validating.

Why it matters: a self-contained token carries readable claims, and anything holding it can read them, so the authorization server chooses its contents knowing who will see them. It is also several hundred characters, sent with every request.

  1. Fetch the key set the API will use.

GET$ISSUER/oauth/jwks Open in console
GET $ISSUER/oauth/jwks HTTP/1.1

Find the access token's kid (EC, ES256) and a separate RSA key (RS256) that signs ID tokens. Note the response header Cache-Control: public, max-age=300.

Why it matters: one key set serves both kinds of token, which is why the type check below matters.

  1. Validate it check by check: btl-lab verify "$TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt. Each line passes in the lesson's order: algorithm from the allowlist, type, key by kid from the issuer's own key set, signature, issuer, audience, time.

Why it matters: no claim is trusted until every check before it has passed, and each check is a setting you choose, not a default you hope for.

  1. Call the photo API: curl -si http://127.0.0.1:8766/photos -H "Authorization: Bearer $TOKEN". Returns 200, and the API's log shows allowed with client_id and jti.

  1. Present the ID token from the same response as if it were an access token: curl -si http://127.0.0.1:8766/photos -H "Authorization: Bearer $ID_TOKEN". Returns 401 invalid_token, logged as unexpected_alg (it is RS256). Run btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt too: the type check fails because typ is JWT.

Why it matters: here the different algorithm catches it first, but nothing requires an issuer to sign ID tokens with a different algorithm. Only the type check reliably keeps another kind of JWT from passing as an access token.

  1. Present a genuine token from another issuer. In Lab Mail, run the same flow with lab-mail-collage (use $ISSUER2, $MAIL_CLIENT_ID and $MAIL_CLIENT_SECRET in the helpers), keep it as MAIL_TOKEN, and call the photo API with it. Returns 401, logged as unknown_kid, after one key set fetch that did not find the key.

Why it matters: the token is real and correctly signed, by someone this API does not trust. The API takes keys only from its own issuer's key set and never from the token.

  1. Publish a new key ahead of use. In Key Management, Generate key with ES256. Fetch the key set again (step 2): two EC keys are published, and new tokens are still signed with the old one.

Why it matters: a key appears in the set before it signs anything, so every API can learn it in advance.

  1. Switch signing. In Access Token Management, edit Default access tokens and choose the new key as its signing key. Save. Get a new token; btl-lab decode shows the new kid. Call the photo API: 200. Its log shows one key set fetch, because the kid was unfamiliar.

Why it matters: refetching on an unknown kid, with a limit on how often, lets the issuer rotate without coordinating with every API.

  1. Check the time rule. Edit Default access tokens and set Maximum lifetime to 60 seconds. Get a token, wait two minutes, and call the API: 401, logged as expired. btl-lab verify shows the time check failing beyond its small clock leeway.

Restore: set Maximum lifetime on Default access tokens back to 3600.

Break it

The cache decides how long a withdrawn key keeps working at an API.

  1. Before this step, keep a token signed with the old key from step 1 as OLD_TOKEN, and confirm the photo API, still running, accepts it.

  2. In Key Management, change the old ES256 key to Retiring, then Disabled. The tenant now treats tokens it signed as revoked.

  3. Call the running API with OLD_TOKEN: still 200, because it holds the old key in its cache and the unfamiliar-kid rule never fires for a known kid.

  4. Restart btl-lab resource (an empty cache) and call again: 401, unknown_kid.

Why it matters: if a signing key leaks, a cache measured in minutes rather than days keeps the window short.

Check your work

Press Check my progress. The checks look for, in order: Ava's first token, tenant.oauth.keys.generate, tenant.oauth.managers.update for the key switch, a token after the switch, and tenant.oauth.keys.disable for the old key.

The API log should show allowed, unexpected_alg, unknown_kid and expired decisions, each with jti and client_id, and never a token.

Cleanup

  1. Keep the new key active and Default access tokens on it. Leave the old key disabled.

  2. Confirm Maximum lifetime on Default access tokens is 3600.

  3. Stop btl-lab resource. Run unset TOKEN ID_TOKEN MAIL_TOKEN OLD_TOKEN RESP.

Missing infrastructure

  • G66 Second lab tenant: this lab uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so an ordinary learner can do only the Lab Photos steps until every learner can have a second lab tenant.

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