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

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. The print job gets a token with its current secret

    Recorded as oauth.token succeeded for lab-print-orders.

  2. Rotate the exposed secret

    Recorded as tenant.oauth.credentials.rotate succeeded.

  3. The old secret stops working at once

    Recorded as oauth.token rejected (invalid_client) for lab-print-orders.

  4. Revoke the token obtained before the rotation

    Recorded as oauth.revoke succeeded (access_token_found) for lab-print-orders.

  5. Move the ID token manager to a new key

    Recorded as tenant.oauth.id_token_managers.update succeeded.

Setup

  1. Press Start on this page.

  2. In OAuth > Flow policy, allow the client credentials grant.

  3. If lab-print-orders does not exist yet, create it in OAuth > Clients: confidential, grant client_credentials, scope prints.create. Store its values in your shell when the secret is shown:

ORDERS_ID=<lab-print-orders client ID>
read -rs ORDERS_SECRET
  1. You also need lab-photo-api's credentials in API_ID and API_SECRET from the sessions lab.

Walkthrough

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

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

  1. In Audit, source Protocol activity, find the oauth.token event. 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.

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

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

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

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

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

  1. Run the job's request with $OLD_SECRET once 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.rotate names you as the actor and lab-print-orders as 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

  1. Clear the old values: unset OLD_SECRET TOKEN.

  2. Keep the new secret in ORDERS_SECRET and 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-orders to private_key_jwt.

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