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.
Sign in to start this lab and check your progress. Log in or create an account.
Publish the next ID token key
Recorded as
tenant.oauth.keys.generatesucceeded.Sign lab-collage's ID tokens with the new key
Recorded as
tenant.oauth.id_token_managers.updatesucceeded.Sign in after the switch
Recorded as
oauth.tokensucceeded forlab-collageabout[email protected].Retire the old key
Recorded as
tenant.oauth.keys.retiresucceeded.Withdraw the old keys in an emergency
Recorded as
tenant.oauth.keys.disablesucceeded.The tenant refuses to retire the key in use
Recorded as
tenant.oauth.keys.retirerejected (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
You need
lab-collagewith itslab-collageID token manager, Ava, the helpers from the authentication request lab, andEXPECTED_ISSUER,EXPECTED_AUDandID_ALGS. Keepbtl-lab callbackrunning.Press Start.
In Key Management, generate an RS256 key (call it key A) and an ES256 key (key C), and note both
kidvalues. Dedicated keys keep this lab away from the keys the default managers and the Token Decoder use.Edit the
lab-collageID token manager to sign with key A, and set its lifetime to 3600 seconds so older tokens stay inside their lifetime while you rotate.In Access Token Management, create a manager named
lab-collage(JWT format, key C, default lifetime) and assign it tolab-collage.Run a fresh sign-in with
signin_url 'openid%20profile'andexchange, and keep the results as the "old" tokens:OLD_IDT=$ID_TOKEN; OLD_AT=$TOKEN; OLD_NONCE=$NONCE.Define this lab's verification. The key set address is your own configuration, and
--jwks-cachekeeps a local copy that is refetched once when a token names an unknownkid:
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
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/jsonKey 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.
Fill the cache:
rm -f "$JWKS_CACHE"; vcheck "$OLD_IDT" "$OLD_NONCE". The output reports that the key set was fetched and ends inACCEPT. Run it again: no fetch this time.
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.
Switch. Edit the
lab-collageID token manager to sign with key B. Sign in again and runvcheck "$ID_TOKEN" "$NONCE". The output reports one key set fetch, the key check passes with key B'skid, and the run ends inACCEPT.
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.
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.
Rotate the access token key the same way: generate ES256 key D, point the
lab-collageaccess 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
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 inACCEPT, because your cache still holds key A and the unknown-kidrule never fires for akidyou already know.Discard the cache as an operator would after a compromise report:
rm "$JWKS_CACHE", then runvcheckagain. The set is fetched and the key check refuses the token.UserInfo with
$OLD_ATnow returns401withBearer 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.
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
Try to retire key B while the
lab-collageID 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.
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
Keep key B on the
lab-collageID token manager and key D on thelab-collageaccess token manager. Set the ID token manager's lifetime back to 300 seconds.Keep
JWKS_CACHEfor the next lab. Rununset OLD_IDT OLD_AT OLD_NONCE.Disabled keys stay listed in Key Management as disabled. Keys cannot be deleted.