OAUTH 2.0 · LAB
Rotate refresh tokens and watch reuse revoke the whole chain
Give the printer a refresh token, let its access token expire, refresh it, and see rotation, scope narrowing, reuse detection, the reuse grace and revocation in real responses.
ReadyUses your lab tenant
The lesson
Builds on: Errors and denied access.
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.
Connect with continued access
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].Refresh without Ava present
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].A refresh cannot ask for more than the grant
Recorded as
oauth.tokenrejected (scope_not_granted) forlab-printer.Reusing a replaced refresh token revokes the chain
Recorded as
oauth.tokenrejected (refresh_replayed) forlab-printer.A retry inside the reuse grace is accepted
Recorded as
oauth.tokensucceeded (refresh_reuse_grace) forlab-printer.The printer revokes the connection
Recorded as
oauth.revokesucceeded (refresh_token_found) forlab-printer.
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
Press Start on this page.
Open OAuth > Flow policy, tick Refresh token under Allowed grants, and save.
In Access Token Management, create a temporary manager
lab-tmp-short-access: Signed JWT, an active key, Maximum lifetime 120 seconds, Refresh token lifetime 3600, Sign-in limit for refresh tokens 3600, and Refresh token reuse grace 0. Save. Refresh lifetimes have a one hour minimum, so this lab describes natural refresh token expiry rather than waiting for it.Open OAuth > Clients > lab-printer, add the Refresh token grant, set Access token manager to
lab-tmp-short-access, and save. Saving revokes the printer's current tokens and remembered consent.Set the
lab-printervariables and definestart_attempt,handle_callbackandredeemas in the Correlating requests and responses lab, plus one refresh helper.
refresh() { curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=refresh_token --data-urlencode "refresh_token=$1" ${2:+-d "scope=$2"} "$ISSUER/oauth/token"; }
Walkthrough
Connect with continued access. Run
start_attempt, changescope=photos.readin the printed address toscope=openid%20photos.read%20offline_access, and sign in as Ava. The consent page now also lists "Keep access when you are not using the app." Approve, runhandle_callback, then exchange the code and keep both tokens.
RESPONSE=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=authorization_code -d "code=$CODE" \
--data-urlencode "redirect_uri=$REDIRECT_URI" -d "code_verifier=$VERIFIER" "$ISSUER/oauth/token")
echo "$RESPONSE" | jq '{expires_in, scope, has_refresh: (.refresh_token != null)}'
TOKEN=$(echo "$RESPONSE" | jq -r .access_token); REFRESH=$(echo "$RESPONSE" | jq -r .refresh_token)
The response has expires_in: 120 and a refresh token.
Why it matters: whether a refresh token is issued is the authorization server's decision. In this tenant it takes the offline_access scope, a Flow policy that allows the grant, and a client registered for it.
Wait two minutes, then use the access token.
GET$ISSUER/oidc/userinfo
Open in console
GET $ISSUER/oidc/userinfo HTTP/1.1
Authorization: Bearer $TOKENThe answer is 401 with Bearer error="invalid_token". The short-lived token did its job and ended.
Refresh without Ava. Save the replacement before anything else.
RESPONSE=$(refresh "$REFRESH"); echo "$RESPONSE" | jq '{expires_in, scope}'
OLD_REFRESH=$REFRESH; REFRESH=$(echo "$RESPONSE" | jq -r .refresh_token); TOKEN=$(echo "$RESPONSE" | jq -r .access_token)
[ "$OLD_REFRESH" != "$REFRESH" ] && echo "rotated"
Why it matters: no code, redirect or PKCE is involved. The printer's client authentication and the stored grant are the whole basis, and each refresh hands over a replacement.
Look at both refresh tokens as the printer. Refresh tokens are private to their client, so only
lab-printercan introspect them.
for t in "$OLD_REFRESH" "$REFRESH"; do curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d token_type_hint=refresh_token --data-urlencode "token=$t" "$ISSUER/oauth/introspect" | jq -c '{active, scope}'; done
The old one is {"active":false}; the new one is active with scope: "openid photos.read offline_access".
Narrow, never widen. Run
RESPONSE=$(refresh "$REFRESH" photos.read), updateREFRESHandTOKENfrom it, and note that the new access token'sscopeisphotos.readwhile the newest refresh token still introspects with the full grant. Then ask for more:refresh "$REFRESH" photos.write | jqanswersinvalid_scope, "A refresh request may only ask for scopes that the refresh token was granted."
Why it matters: a refresh token carries forward what Ava approved. Broader access needs a new authorization.
A client that forgot to save its replacement. Present
OLD_REFRESH:invalid_grant, "The refresh token was already used, so every token from the same sign-in has been revoked." Now present the newestREFRESH:invalid_grant, "The refresh token was revoked."btl-lab introspect "$TOKEN"shows the newest access token is inactive too.
Why it matters: only one party should ever hold the newest refresh token. Reuse revokes the whole chain, for the thief and the printer alike, until Ava reconnects.
The reuse grace. A lost refresh response leaves the printer with only the old token. Open
lab-tmp-short-access, set Refresh token reuse grace to 30 seconds and save. Run step 1 again for a fresh chain, run step 3, then within 30 seconds presentOLD_REFRESHagain:200, and Audit records reasonrefresh_reuse_grace. Wait a minute and present it once more:invalid_grantwith the reuse message.
Why it matters: a grace window lets a client recover from a response lost moments ago without weakening reuse detection outside the window. The tenant chooses the trade-off per token manager, and zero means any second use is a replay.
Restore: in lab-tmp-short-access, set Refresh token reuse grace back to 0 and save.
Disconnect. Run step 1 again for a fresh chain, then revoke it as the printer would when Ava disconnects.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d token_type_hint=refresh_token --data-urlencode "token=$REFRESH" "$ISSUER/oauth/revoke"
refresh "$REFRESH" | jq
The refresh answers invalid_grant, "The refresh token was revoked." The printer marks the connection as needing attention and asks Ava to reconnect; retrying cannot help.
Break it
Two workers at once. On a fresh chain, run the same refresh twice in parallel:
refresh "$REFRESH" & refresh "$REFRESH" & wait. One answer has new tokens; the other isinvalid_grantfor reuse, and the winner's new tokens are revoked as well. A client using rotation lets only one refresh per connection run at a time.A change on the account. On a fresh chain, open Users > Ava Archer and Lock her, then refresh:
invalid_grant, asking for Ava to sign in again.
Restore: open Users > Ava Archer and Unlock her.
Check your work
Press Check my progress. In tenant Audit you can also find tenant.oauth.policy.update, tenant.oauth.managers.create and tenant.oauth.managers.update, tenant.oauth.clients.update, oauth.introspect succeeded refresh_token_found, oauth.token rejected refresh_revoked, and tenant.users.lock and tenant.users.unlock.
Cleanup
In OAuth > Clients > lab-printer, set Access token manager back to Default access tokens and save. Keep the Refresh token grant on the printer and in Flow policy.
Delete the
lab-tmp-short-accessmanager.Confirm Ava is unlocked.