IDENTITY SECURITY · LAB
Rotate an exposed client secret, then rehearse a planned key rotation
Inventory lab-print-orders' secret, treat it as exposed and rotate it revoke-first, deal with the tokens it already obtained, then rotate the ID token signing key with a planned overlap.
Partly readyUses your lab tenant
The lesson
Builds on: Forged tokens and stolen signing keys.
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.
- G8 Client authentication beyond `client_secret_basic` and `none`
- G10 Client secret rotation overlap
- G52 Governance and usage reporting: ownership metadata, filterable last sign-in, per-client last use and usage inventory, report-only enforcement
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.
The print job gets a token with its current secret
Recorded as
oauth.tokensucceeded forlab-print-orders.Rotate the exposed secret
Recorded as
tenant.oauth.credentials.rotatesucceeded.The old secret stops working at once
Recorded as
oauth.tokenrejected (invalid_client) forlab-print-orders.Revoke the token obtained before the rotation
Recorded as
oauth.revokesucceeded (access_token_found) forlab-print-orders.Move the ID token manager to a new key
Recorded as
tenant.oauth.id_token_managers.updatesucceeded.
Setup
Press Start on this page.
In OAuth > Flow policy, allow the client credentials grant.
If
lab-print-ordersdoes not exist yet, create it in OAuth > Clients: confidential, grantclient_credentials, scopeprints.create. Store its values in your shell when the secret is shown:
ORDERS_ID=<lab-print-orders client ID>
read -rs ORDERS_SECRET
You also need
lab-photo-api's credentials inAPI_IDandAPI_SECRETfrom the sessions lab.
Walkthrough
Write the inventory row for
lab-print-orders' secret: what it unlocks (prints.create, placing print orders with no user involved), which systems use it (your shell, standing in for the back-office job), where it is stored, who owns it, when it was last replaced, and how to replace it.
Why it matters: before replacing a secret you need to know what will break. Write down every copy you know of. The lesson's forgotten script on a laptop was the copy nobody listed.
Use the secret as the job normally would, and keep the access token.
TOKEN=$(curl -s -u "$ORDERS_ID:$ORDERS_SECRET" -d grant_type=client_credentials -d scope=prints.create "$ISSUER/oauth/token" | jq -r .access_token)
btl-lab decode "$TOKEN"
Why it matters: there is no user here. Whoever holds the secret is the print job, from anywhere, which is what makes an exposed client secret urgent.
In Audit, source Protocol activity, find the
oauth.tokenevent. Its actor is the OAuth client, recorded by client ID, never by secret value.
Why it matters: the lesson's clue that a key is in someone else's hands is how it is used. Recording which credential each request used, by identifier, is what makes unusual use visible.
Note: for this exercise, assume the secret was pasted into a support ticket that several people saw. It can create print orders, so you decide on an emergency rotation.
Rotate the secret in OAuth > Clients. The new secret is shown once. Your tenant has no overlap for client secrets, so the old one stops working the moment the new one exists. Store the new one.
OLD_SECRET="$ORDERS_SECRET"
read -rs ORDERS_SECRET
Why it matters: in an emergency the order flips: revoke first, then deploy the new secret, accepting a short outage in between.
Try the old secret, then the new one.
curl -s -u "$ORDERS_ID:$OLD_SECRET" -d grant_type=client_credentials -d scope=prints.create "$ISSUER/oauth/token" | jq .
curl -s -u "$ORDERS_ID:$ORDERS_SECRET" -d grant_type=client_credentials -d scope=prints.create "$ISSUER/oauth/token" | jq '{token_type, expires_in, scope}'
Why it matters: the old secret is refused with invalid_client, and the job works again with the new one. Any copy you did not list fails now; in the lesson, that failure was the first anyone knew of it.
Check what the old secret already did. Introspect the token from step 2 as the photo API: it is still active. Revoke it as the print job, then introspect again.
btl-lab introspect "$TOKEN" --client-env API
curl -s -u "$ORDERS_ID:$ORDERS_SECRET" --data-urlencode token="$TOKEN" "$ISSUER/oauth/revoke" | jq .
btl-lab introspect "$TOKEN" --client-env API
Why it matters: rotation stops future use and nothing more. Tokens obtained with the exposed secret stay valid until they expire unless they are revoked as well.
Now rehearse a planned rotation on the ID token signing key, the one the Forged tokens lab left alone. In Key Management, generate a new key with the same algorithm as the current ID token key. Edit the ID token manager to sign with it, then retire the old key and leave it published.
Why it matters: a planned rotation overlaps old and new so nothing breaks. Verifiers keep the old key until their cached key set refreshes, and only then is it safe to remove.
After at least an hour (longer than any verifier you know caches the key set), disable the old ID token key. Record the dates in the inventory.
Why it matters: routine rotation is rehearsal. A team that has done it calmly, with the steps written down, does it faster when a key has actually leaked.
Break it
Run the job's request with
$OLD_SECRETonce more, as a forgotten copy would. It fails every time. The fix is to update that copy, not to bring the old secret back.
Check your work
Check my progress confirms the token request, the rotation, the refused old secret, the revoked earlier token, and the ID token manager change.
In Audit, source OAuth management,
tenant.oauth.credentials.rotatenames you as the actor andlab-print-ordersas the subject, without any secret value.Your inventory row has every field filled in, including the rotation date and where the new secret now lives.
Cleanup
Clear the old values:
unset OLD_SECRET TOKEN.Keep the new secret in
ORDERS_SECRETand the new keys active.
Missing infrastructure
G10: a client can hold only one secret, so a planned rotation without an outage is not possible. Once it exists, the lab will add a second secret, move the job to it, confirm the old one is no longer used, and then remove it.
G52: there is no record of when each secret was last used, so you cannot confirm that nothing still presents the old one. Once it exists, the lab will read the last-used time before removing the old secret.
G8: clients authenticate only with a shared secret, so "fewer secrets to leak" with a private key that never leaves the server cannot be practiced. Once it exists, the lab will switch
lab-print-orderstoprivate_key_jwt.