OAUTH 2.0 · LAB
Rotate a client secret without overlap, then signing keys with overlap
Reproduce the lesson's rotation outage with two servers, respond to a leaked secret in the right order, and run an add, use, retire, remove rotation on the tenant's signing keys.
Partly readyUses your lab tenant
The lesson
Builds on: Secrets and signed assertions.
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.
- G10 Client secret rotation overlap
- G8 Client authentication beyond `client_secret_basic` and `none`
- G56 Client JWKS (`jwks` / `jwks_uri` per client)
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.
Replace the print-orders secret
Recorded as
tenant.oauth.credentials.rotatesucceeded.A server still using the old secret is refused
Recorded as
oauth.tokenrejected (invalid_client) forlab-print-orders.A token issued before the rotation is still active
Recorded as
oauth.introspectsucceeded (other_client_token_found) forlab-photo-api.Revoke everything issued to the leaked client
Recorded as
tenant.oauth.clients.updatesucceeded.Publish a new signing key
Recorded as
tenant.oauth.keys.generatesucceeded.Remove the old signing key
Recorded as
tenant.oauth.keys.disablesucceeded.
Setup
Open two terminals, A and B. Each is one of the printer's servers. In both, set
ISSUER,CLIENT_IDand the currentCLIENT_SECRETforlab-print-orders, andAPI_IDandAPI_SECRETforlab-photo-api. Define one helper in both.
get_token() { curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=client_credentials -d scope=prints.create "$ISSUER/oauth/token"; }
Press Start on this page.
Walkthrough
Both servers get a token. In terminal A, keep it:
A_TOKEN=$(get_token | jq -r .access_token). In terminal B,get_token | jq .scope.
Replace the secret. Open Access Token Management > Client authentication secret, choose
lab-print-ordersand Rotate client secret. Load the new value into terminal B only:read -rs CLIENT_SECRET.
Terminal A, not yet updated, asks for a token:
get_token | jqanswersinvalid_client. Terminal B: a token.
Why it matters: this tenant allows one secret per client, so the old one stopped at once. Rotation here is a coordinated switch with a short failure window, the lesson's outage on a small scale.
Find the servers still using the old secret. In Audit, filter for
oauth.token, outcome rejected, clientlab-print-orders, reasoninvalid_client. Each row has a request ID and a time. Audit cannot tell you which secret a request presented, or when each secret was last used (G10).
Tokens issued before the rotation still work: in terminal A,
btl-lab introspect "$A_TOKEN"answersactive: true.
Respond to a leak, in the lesson's order.
Revoke first: rotate the secret again and do not deploy the new value yet. The leaked secret is now useless, and so is every server's copy. Both terminals now get
invalid_client.End what was already issued: open OAuth > Clients > lab-print-orders, untick Enabled and save, which revokes all its tokens.
btl-lab introspect "$A_TOKEN"now answers{"active": false}.Tick Enabled again and save, then load the newest secret into both terminals with
read -rs CLIENT_SECRET. Both get tokens.
Why it matters: when someone else may hold the credential, a short, honest outage is better than leaving it valid while the deployment proceeds. Tokens issued before the revocation do not disappear on their own, so they are ended separately.
Restore: confirm lab-print-orders is Enabled and both terminals hold the newest secret.
Overlap, on the tenant's own access token signing keys. Client key pairs need G8 and G56; the tenant's keys show the same pattern for real.
In Key Management, choose Generate key with ES256.
btl-lab discover "$ISSUER"now lists two keys. Get a token in terminal A asOLD_KEY_TOKEN;btl-lab decodeshows the oldkid.Try to retire the old key now. It is refused: a key that a token manager still signs with cannot be retired.
In Access Token Management, edit Default access tokens to sign with the new key, and do the same for any other manager still using the old one. New tokens carry the new
kid, whilebtl-lab verify "$OLD_KEY_TOKEN" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwtstill passes, because the old key is still published.In Key Management, retire the old key. It stays in the JWKS while tokens it signed may still be valid.
Disable the old key. It leaves the JWKS,
btl-lab verifyonOLD_KEY_TOKENnow fails to find itskid, andbtl-lab introspect "$OLD_KEY_TOKEN"answers{"active": false}.
Why it matters: publishing the new key before signing with it, and removing the old one only after its tokens are no longer needed, means there is never a moment when the only valid credential is one a relying party cannot check.
Planned walkthrough
These steps need two concurrent client secrets (G10), and client key sets with private_key_jwt (G8, G56).
Overlapping secret rotation with zero failures. Add a second secret to
lab-print-orders; both terminals keep working with the old one. Deploy the new secret to terminal B, then A. Confirm in the client's view and in Audit that the old secret's last use stopped. Remove the old secret. No request fails.Client key rotation through a
jwks_uri: publish a second public key in your own key set, switch signing to it, let the last old assertion expire, and remove the old key, without changing the registration.
Break it
Steps 3, 6.1 and 7.2 are the deliberate failures.
Check your work
Press Check my progress. In tenant Audit you can also find a second tenant.oauth.credentials.rotate, the tenant.oauth.clients.update that re-enabled the client, tenant.oauth.managers.update for the signing key change, and tenant.oauth.keys.retire.
Cleanup
Make sure both terminals hold the newest secret.
Leave the new signing key active.
Missing infrastructure
G10, client secret rotation overlap. Two concurrent secrets per client with Add, Deploy, Confirm and Remove, and a last-used time per secret shown in the client and recorded in Audit. With it, the planned rotation runs with zero failed requests.
G8 and G56, client key pairs.
private_key_jwtwith a registeredjwks_uri, so the client rotates its own keys by publishing and retiring them without touching the registration.