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 a refresh token family, tune its reuse grace, and end it

Rotate Ava's refresh token, narrow and widen scope, see strict reuse end the family, allow a few seconds of grace for a lost response, then restore strict mode.

ReadyUses your lab tenant

The lesson

Builds on: Scopes and audiences.

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. Start a refresh token family for Ava

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

  2. Be refused a wider scope on refresh

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

  3. Present a used refresh token with grace 0

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

  4. Retry a lost refresh inside the reuse grace

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

  5. Set a new password for Ava

    Recorded as tenant.users.credentials.set succeeded.

  6. See the family end after the password change

    Recorded as oauth.token rejected (refresh_revoked) 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. Choose Lab Photos as the lab tenant and press Start.

  2. In Flow policy, allow the refresh token grant and save. On lab-printer, allow the refresh token grant too, and confirm photos.share is in its Assigned scopes.

  3. In Access Token Management, create a manager lab-tmp-refresh: Signed JWT, the current ES256 key, Maximum lifetime 600, Refresh token lifetime in seconds 86400 (the idle lifetime), Sign-in limit for refresh tokens in seconds 604800 (the absolute lifetime), Refresh token reuse grace in seconds 0. Under Assign a client, assign it to lab-printer.

  4. Use the variables and helpers from Present an access token correctly, and extend exchange to keep REFRESH=$(jq -r '.refresh_token // empty' <<<"$RESP"). Add a refresh helper and an introspection helper:

refresh() {  # $1 = refresh token, $2 = optional narrower scope
  RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=refresh_token \
    --data-urlencode "refresh_token=$1" ${2:+--data-urlencode "scope=$2"})
  TOKEN=$(jq -r '.access_token // empty' <<<"$RESP"); REFRESH=$(jq -r '.refresh_token // empty' <<<"$RESP")
  jq 'del(.access_token, .refresh_token, .id_token)' <<<"$RESP"
}
active() { curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/introspect" --data-urlencode "token=$1" | jq '{active, scope, exp}'; }

Walkthrough

  1. Start a family. authorize "openid photos.read photos.share offline_access", sign in as Ava, exchange. The response includes a refresh token. Keep it: RT1=$REFRESH.

Why it matters: a refresh token is issued only when the client asks for continued access and both Flow policy and the client allow the grant. The tenant stores only a hash of it, so a copy of its database holds nothing that could be presented.

  1. Rotate. Run refresh "$RT1", then RT2=$REFRESH. You get a new access token and a new refresh token.

POST$ISSUER/oauth/token Open in console
POST $ISSUER/oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=$RT1

Why it matters: rotation replaces the token in one step and remembers the old one as used.

  1. Compare the two: active "$RT1" shows {"active": false}; active "$RT2" is active with exp about a day ahead.

Why it matters: each successful refresh moves the idle deadline forward, and no token in the family outlives the absolute deadline of seven days.

  1. Narrow, then come back. refresh "$RT2" photos.read gives an access token with only photos.read; keep RT3=$REFRESH. Then refresh "$RT3" with no scope: the access token is back to openid photos.read photos.share.

Why it matters: a narrow request today does not shrink what the replacement refresh token may ask for next time.

  1. Try to widen: refresh "$REFRESH" "photos.read photos.delete". Returns 400 invalid_scope; Audit reason scope_not_granted.

Why it matters: a refresh token can never add scope. That needs Ava's approval again.

  1. Present a used token while the reuse grace is 0. Run refresh "$RT2". Returns 400 invalid_grant saying every token from that sign-in was revoked. Then try the newest refresh token: also invalid_grant (refresh_revoked). active "$TOKEN" on the newest access token shows {"active": false}.

Why it matters: the server cannot tell a client mistake from a stolen copy, so it ends the family, including the access tokens it can still reach.

  1. Recover from a lost response with a short grace. Edit lab-tmp-refresh and set Refresh token reuse grace in seconds to 10. Save. Start a fresh family (step 1) and keep RT1=$REFRESH. Now refresh and throw the response away, as if the connection dropped:

curl -s -o /dev/null -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=refresh_token --data-urlencode "refresh_token=$RT1"
refresh "$RT1"   # retry within 10 seconds

The retry succeeds with new tokens. Audit records oauth.token succeeded with reason refresh_reuse_grace. Wait 15 seconds and run refresh "$RT1" once more: invalid_grant, refresh_replayed, and the family ends.

Why it matters: no standard defines a grace period. It saves a person from reconnecting after a network fault, and its cost is real: a copy presented inside the window is not detected either. That is why it stays a few seconds: any refresh token rotated within that many seconds can be presented again, and after that every reuse is a replay.

Restore: set Refresh token reuse grace in seconds on lab-tmp-refresh back to 0, so any second use is treated as theft again.

  1. End a family from an account event. Start a fresh family (step 1). In User Management, set a new password for Ava. Then refresh "$REFRESH": invalid_grant (refresh_revoked).

Why it matters: a password reset is one of the events that should end families. A person removing the printer from a connected applications page would end the grant the same way.

Note: there is no connected applications page for people yet (G40), so this lab uses an administrator's password change as the account event.

  1. Present the token as a different client. Using lab-print-orders credentials, send a refresh_token grant with a current lab-printer refresh token. It is refused with unauthorized_client, because that client has no refresh grant. Even if it had one, the lookup is per client and would find nothing.

Why it matters: a refresh token is bound to the client it was issued to. Any other client presenting it is refused.

Break it

Run two refreshes of the same token at the same moment, with the grace at 0:

for i in 1 2; do curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=refresh_token --data-urlencode "refresh_token=$REFRESH" | jq -c '{error, error_description}' & done; wait

One succeeds and the other is treated as reuse, which ends the family. The replacement is conditional on the token still being current, inside one transaction, so the family cannot split into two branches. This is why a client runs one refresh per connection at a time.

Check your work

Press Check my progress. The checks look for, in order: the first family for Ava, scope_not_granted, refresh_replayed with grace 0, a success with refresh_reuse_grace, tenant.users.credentials.set for Ava, and refresh_revoked after it.

Audit also shows tenant.oauth.managers.update twice for the grace change and its restore.

Cleanup

  1. Confirm lab-tmp-refresh shows a reuse grace of 0.

  2. Assign Default access tokens back to lab-printer, then delete lab-tmp-refresh.

  3. Store Ava's new password; later labs sign in with it. Run unset TOKEN REFRESH RT1 RT2 RT3 RESP.

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