OPENID CONNECT · LAB
Request offline access and compare refreshed ID tokens
Ask separately for photos.read with offline_access and prompt=consent, refresh, compare the original and refreshed ID tokens claim by claim, then show that a refresh is not proof that anyone is present.
ReadyUses your lab tenant
The lesson
Builds on: Connecting a sign-in to an account.
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.
Approve offline access on the consent page
Recorded as
oauth.authorizesucceeded (code_issued) forlab-collageabout[email protected].Receive a refresh token with the ID token
Recorded as
oauth.tokensucceeded forlab-collage.Sign out of the tenant
Recorded as
account.sign_outsucceeded (signed_out) about[email protected].Refresh successfully while signed out
Recorded as
oauth.tokensucceeded forlab-collage.Present a rotated refresh token again
Recorded as
oauth.tokenrejected (refresh_replayed) forlab-collage.
Setup
In Lab Photos, open OAuth > Flow policy and make sure the
refresh_tokengrant is allowed.Open OAuth > Clients > lab-collage. Make sure it has the
refresh_tokengrant and thatphotos.readandoffline_accessare among its allowed scopes. The Lab Photos preset createsphotos.readas a common scope.Open OAuth > ID token managers, open the manager assigned to
lab-collage, and confirm it issues an ID token on refresh (on by default). Open the access token manager assigned tolab-collageand confirm Refresh token reuse grace is 0 seconds, the default.source ~/btl-oidc.sh, runbtl-lab callbackin a second terminal, and press Start.
Walkthrough
Sign in for sign-in only, with
SCOPE="openid profile email". Redeem:"refresh_token": false.
Why it matters: a sign-in that did not ask for offline access gets none, which suits a sign-in.
The calendar request. Ask separately for the photo connection and a deliberate consent decision:
SCOPE="openid photos.read offline_access"
signin prompt=consent
The consent page lists both scopes, even though Ava approved lab-collage before. Approve and redeem: now "refresh_token": true. Validate the ID token against this NONCE and keep it.
redeem '<code>'
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE" && ID1=$ID_TOKEN
Why it matters: long-term access needs a deliberate decision, and the client checks the response instead of assuming a refresh token arrived.
Refresh, exactly as an OAuth client would:
RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=refresh_token --data-urlencode "refresh_token=$REFRESH")
OLD_REFRESH=$REFRESH; REFRESH=$(jq -r .refresh_token <<<"$RESP"); ID2=$(jq -r .id_token <<<"$RESP")
jq '{token_type, scope, refresh_token: (.refresh_token != null), id_token: (.id_token != null)}' <<<"$RESP"
A new access token, a new refresh token, and an id_token.
Why it matters: OpenID Connect adds nothing to the refresh request and one field to the response.
Compare the two ID tokens, then validate the refreshed one with no nonce.
diff <(part "$ID1" | jq -S '{iss, sub, aud, auth_time, nonce}') <(part "$ID2" | jq -S '{iss, sub, aud, auth_time, nonce}')
btl-lab verify "$ID2" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id
Only nonce differs, absent in ID2. iat and exp are new. Then assert the rules from the lesson: same iss, sub and aud, and auth_time not later than ID1's.
Why it matters: identity claims cannot move, and auth_time still describes the original authentication, not the refresh.
Refresh is not presence. In the private window open
$ISSUER/accountand choose Sign out. Then runsignin prompt=none:error=login_required. Refresh again with$REFRESHusing the step 3 command: it succeeds.
Why it matters: a successful refresh proves the grant is alive, not that a person is at a browser, so it must never create or extend an app session.
Turn the ID token manager's "issue an ID token on refresh" off, refresh again, and see no
id_tokenin the response. Turn it back on.
Restore: switch "issue an ID token on refresh" back on and save.
Why it matters: a provider may leave the ID token out of a refresh response, so the client must not depend on one.
Break it
Present a rotated refresh token. Refresh with
$OLD_REFRESH:400 {"error":"invalid_grant"}. Then refresh with the newest$REFRESH: alsoinvalid_grant, because the replay ended the whole token family. This is the calendar's "mark the connection for attention" case, and reconnecting means a new authorization, not a sign-out.A consent shortcut, contained to your own client. Set
lab-collage's consent mode to Skip and requestopenid photos.read offline_accesswithoutprompt=consent: a refresh token arrives with no screen at all. Compare that with the lesson's rule that offline access needs a consent decision.
Restore: set lab-collage's consent mode back to Remember at once.
Check your work
Press Check my progress. The checks look for the code issued after your consent, the exchange that returned a refresh token, Ava's sign-out, a successful refresh after it, and the rejected replay.
Your diff showed only nonce changing among iss, sub, aud, auth_time and nonce.
Cleanup
Consent mode Remember, ID token on refresh on.
Revoke any refresh token you still hold:
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" --data-urlencode "token=$REFRESH" -o /dev/null -w '%{http_code}\n'
Set
SCOPE="openid profile email"again.