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

Rotate lab-collage's signing keys while a relying party keeps a cached key set

Publish a next key, switch lab-collage to it, watch a cached key set refetch on an unknown kid, then retire and disable the old keys and see what still verifies.

ReadyUses your lab tenant

The lesson

Builds on: Expiration and nonce checks.

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. Publish the next ID token key

    Recorded as tenant.oauth.keys.generate succeeded.

  2. Sign lab-collage's ID tokens with the new key

    Recorded as tenant.oauth.id_token_managers.update succeeded.

  3. Sign in after the switch

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

  4. Retire the old key

    Recorded as tenant.oauth.keys.retire succeeded.

  5. Withdraw the old keys in an emergency

    Recorded as tenant.oauth.keys.disable succeeded.

  6. The tenant refuses to retire the key in use

    Recorded as tenant.oauth.keys.retire rejected (key_in_use).

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 with its lab-collage ID token manager, Ava, the helpers from the authentication request lab, and EXPECTED_ISSUER, EXPECTED_AUD and ID_ALGS. Keep btl-lab callback running.

  2. Press Start.

  3. In Key Management, generate an RS256 key (call it key A) and an ES256 key (key C), and note both kid values. Dedicated keys keep this lab away from the keys the default managers and the Token Decoder use.

  4. Edit the lab-collage ID token manager to sign with key A, and set its lifetime to 3600 seconds so older tokens stay inside their lifetime while you rotate.

  5. In Access Token Management, create a manager named lab-collage (JWT format, key C, default lifetime) and assign it to lab-collage.

  6. Run a fresh sign-in with signin_url 'openid%20profile' and exchange, and keep the results as the "old" tokens: OLD_IDT=$ID_TOKEN; OLD_AT=$TOKEN; OLD_NONCE=$NONCE.

  7. Define this lab's verification. The key set address is your own configuration, and --jwks-cache keeps a local copy that is refetched once when a token names an unknown kid:

export JWKS_CACHE="$HOME/jwks-lab-photos.json"
vcheck() { btl-lab verify "$1" --issuer "$EXPECTED_ISSUER" --audience "$EXPECTED_AUD" --algs "${ALGS:-$ID_ALGS}" --type id --nonce "$2" --jwks-cache "$JWKS_CACHE" --max-iat-age 3600; }

Walkthrough

  1. Read the key set as a relying party does:

GET$ISSUER/oauth/jwks Open in console
GET $ISSUER/oauth/jwks HTTP/1.1
Accept: application/json

Key A and key C are listed beside the tenant's default keys. Confirm that btl-lab decode "$OLD_IDT" names key A's kid and btl-lab decode "$OLD_AT" names key C's.

Why it matters: one set holds keys for different jobs. The relying party picks a key by kid and uses each key with one algorithm only, never searching for some other key that might work.

  1. Fill the cache: rm -f "$JWKS_CACHE"; vcheck "$OLD_IDT" "$OLD_NONCE". The output reports that the key set was fetched and ends in ACCEPT. Run it again: no fetch this time.

  1. Publish the next key. Generate RS256 key B. Fetch the JWKS again: A and B are both published. Your cache does not have B yet, and nothing is signed with it.

  1. Switch. Edit the lab-collage ID token manager to sign with key B. Sign in again and run vcheck "$ID_TOKEN" "$NONCE". The output reports one key set fetch, the key check passes with key B's kid, and the run ends in ACCEPT.

Why it matters: an unknown kid triggers one refetch, and the rotation needs no change at the relying party. Nobody had to do anything, and no one signing in noticed.

  1. Retire key A, which no manager uses now. The JWKS still lists A while it is retiring, and vcheck "$OLD_IDT" "$OLD_NONCE" still accepts.

Why it matters: retiring keeps a key published while tokens it signed may still be in circulation.

  1. Rotate the access token key the same way: generate ES256 key D, point the lab-collage access token manager at D, and retire C. The old access token still works:

curl -s "$ISSUER/oidc/userinfo" -H "Authorization: Bearer $OLD_AT" | jq .sub
  1. Emergency withdrawal, the lesson's leaked key. Disable key A and key C.

    • The JWKS no longer lists either key.

    • vcheck "$OLD_IDT" "$OLD_NONCE" still ends in ACCEPT, because your cache still holds key A and the unknown-kid rule never fires for a kid you already know.

    • Discard the cache as an operator would after a compromise report: rm "$JWKS_CACHE", then run vcheck again. The set is fetched and the key check refuses the token.

    • UserInfo with $OLD_AT now returns 401 with Bearer error="invalid_token": disabling key C revoked the access tokens it signed.

Why it matters: your refresh schedule decides how long a withdrawn key keeps working at your relying party. Refresh in minutes, and keep a way to discard the cached set at once.

  1. Which key signed what. Disabling key A did not revoke $OLD_AT; disabling key C did, because key C signed it. Revocation on disable follows the key that signed each token.

Break it

  1. Try to retire key B while the lab-collage ID token manager uses it. The tenant refuses and asks you to move the manager to another active key first. That is the order a planned rotation follows: publish, switch, then retire.

  1. Your list decides. Run ALGS=PS256 vcheck "$ID_TOKEN" "$NONCE". It stops at the algorithm check. Neither the token header nor the provider's metadata chooses the algorithms you accept.

Check your work

Press Check my progress. It looks for, in order: a key generated, a tenant.oauth.id_token_managers.update that switches keys, a lab-collage token request after it, a key retired, a key disabled, and the rejected retire of the key in use.

Audit also shows tenant.oauth.managers.create, .assign and .update for the access token manager. The UserInfo call with the revoked access token is in Logs as oidc.userinfo rejected with invalid_token, and your key set fetches appear there as oauth.jwks requests.

Cleanup

  1. Keep key B on the lab-collage ID token manager and key D on the lab-collage access token manager. Set the ID token manager's lifetime back to 300 seconds.

  2. Keep JWKS_CACHE for the next lab. Run unset OLD_IDT OLD_AT OLD_NONCE.

  3. Disabled keys stay listed in Key Management as disabled. Keys cannot be deleted.

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