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

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.

  1. Approve offline access on the consent page

    Recorded as oauth.authorize succeeded (code_issued) for lab-collage about [email protected].

  2. Receive a refresh token with the ID token

    Recorded as oauth.token succeeded for lab-collage.

  3. Sign out of the tenant

    Recorded as account.sign_out succeeded (signed_out) about [email protected].

  4. Refresh successfully while signed out

    Recorded as oauth.token succeeded for lab-collage.

  5. Present a rotated refresh token again

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

Setup

  1. In Lab Photos, open OAuth > Flow policy and make sure the refresh_token grant is allowed.

  2. Open OAuth > Clients > lab-collage. Make sure it has the refresh_token grant and that photos.read and offline_access are among its allowed scopes. The Lab Photos preset creates photos.read as a common scope.

  3. 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 to lab-collage and confirm Refresh token reuse grace is 0 seconds, the default.

  4. source ~/btl-oidc.sh, run btl-lab callback in a second terminal, and press Start.

Walkthrough

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

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

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

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

  1. Refresh is not presence. In the private window open $ISSUER/account and choose Sign out. Then run signin prompt=none: error=login_required. Refresh again with $REFRESH using 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.

  1. Turn the ID token manager's "issue an ID token on refresh" off, refresh again, and see no id_token in 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

  1. Present a rotated refresh token. Refresh with $OLD_REFRESH: 400 {"error":"invalid_grant"}. Then refresh with the newest $REFRESH: also invalid_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.

  2. A consent shortcut, contained to your own client. Set lab-collage's consent mode to Skip and request openid photos.read offline_access without prompt=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

  1. Consent mode Remember, ID token on refresh on.

  2. 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'
  1. Set SCOPE="openid profile email" again.

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