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

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.

  1. Connect with continued access

    Recorded as oauth.token succeeded for lab-printer about [email protected].

  2. Refresh without Ava present

    Recorded as oauth.token succeeded for lab-printer about [email protected].

  3. A refresh cannot ask for more than the grant

    Recorded as oauth.token rejected (scope_not_granted) for lab-printer.

  4. Reusing a replaced refresh token revokes the chain

    Recorded as oauth.token rejected (refresh_replayed) for lab-printer.

  5. A retry inside the reuse grace is accepted

    Recorded as oauth.token succeeded (refresh_reuse_grace) for lab-printer.

  6. The printer revokes the connection

    Recorded as oauth.revoke succeeded (refresh_token_found) for lab-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

  1. Press Start on this page.

  2. Open OAuth > Flow policy, tick Refresh token under Allowed grants, and save.

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

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

  5. Set the lab-printer variables and define start_attempt, handle_callback and redeem as 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

  1. Connect with continued access. Run start_attempt, change scope=photos.read in the printed address to scope=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, run handle_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.

  1. Wait two minutes, then use the access token.

GET$ISSUER/oidc/userinfo Open in console
GET $ISSUER/oidc/userinfo HTTP/1.1
Authorization: Bearer $TOKEN

The answer is 401 with Bearer error="invalid_token". The short-lived token did its job and ended.

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

  1. Look at both refresh tokens as the printer. Refresh tokens are private to their client, so only lab-printer can 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".

  1. Narrow, never widen. Run RESPONSE=$(refresh "$REFRESH" photos.read), update REFRESH and TOKEN from it, and note that the new access token's scope is photos.read while the newest refresh token still introspects with the full grant. Then ask for more: refresh "$REFRESH" photos.write | jq answers invalid_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.

  1. 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 newest REFRESH: 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.

  1. 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 present OLD_REFRESH again: 200, and Audit records reason refresh_reuse_grace. Wait a minute and present it once more: invalid_grant with 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.

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

  1. 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 is invalid_grant for 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.

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

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

  2. Delete the lab-tmp-short-access manager.

  3. Confirm Ava is unlocked.

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