OPENID CONNECT · LAB
Separate the provider session, the app session and the ID token
Shorten the ID token to one minute and the tenant session to fifteen, then show that an expired ID token ends nothing and that single sign-on lasts exactly as long as the provider's session.
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.
Shorten the ID token lifetime
Recorded as
tenant.oauth.id_token_managers.updatesucceeded.Shorten the browser session lifetime
Recorded as
tenant.oauth.policy.updatesucceeded.Sign in on the tenant's own page
Recorded as
account.sign_insucceeded (signed_in) about[email protected].Get a code from the provider session with no page
Recorded as
oauth.authorizesucceeded (code_issued) forlab-collageabout[email protected].Use the access token once at UserInfo
Recorded as
oidc.userinfosucceeded (userinfo_served).See prompt=none fail after the provider session ends
Recorded as
oauth.authorizerejected (login_required) forlab-collage.
Setup
Three clocks run in every sign-in: the provider's session, the relying party's session and the ID token. All three are real here, and two of them are tenant settings.
Press Start on this page.
In Lab Photos, open OAuth > ID token managers, open the manager assigned to
lab-collage, note its lifetime, and set it to 60 seconds.Open OAuth > Flow policy, note the browser session lifetime, and set it to 900 seconds, the minimum.
In a private window, open
$ISSUER/accountand sign out if a session is open. In your terminal,source ~/btl-oidc.sh.
Walkthrough
Watch the provider defend against session fixation. In the private window open
$ISSUER/loginand the developer tools' cookie view. Note any__Host-btl-oauth-sessionvalue (there may be none). Sign in as Ava and look again: a new value, withSecure,HttpOnly,SameSite=LaxandPath=/. Write down the time.
Why it matters: a session identifier that existed before sign-in must never become the signed-in session. The lesson's printer replaces its anonymous session the same way.
Run a sign-in in the same window. No page appears, because the session from step 1 answers.
signin
redeem '<code>'
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE"
part "$ID_TOKEN" | jq '{iat, exp, auth_time, life: (.exp - .iat), acr, sid}'
FIRST_AUTH_TIME=$(part "$ID_TOKEN" | jq .auth_time)
life is 60. auth_time is the step 1 sign-in, earlier than iat. acr and sid are null.
Why it matters: your session record stores auth_time, and records "none in this ID token" for acr and sid rather than inventing them.
Use the access token once, then drop it.
curl -s -H "Authorization: Bearer $TOKEN" "$ISSUER/oidc/userinfo" | jq '{sub, email}'
unset TOKEN
Why it matters: a sign-in that only needed the profile has no further use for the access token, so it is not kept.
Build your app session record from the validated token. It holds an identifier pointing to server-side state, never the token's claims in a cookie.
NOW=$(date +%s)
jq -n --arg sub "$(part "$ID_TOKEN" | jq -r .sub)" --arg iss "$ISSUER" --argjson at "$FIRST_AUTH_TIME" --argjson now "$NOW" \
'{session: "s-local-1", account: "acct-8812", issuer: $iss, subject: $sub, auth_time: $at, acr: null, sid: null,
created: $now, idle_deadline: ($now + 7200), absolute_deadline: ($now + 604800)}' > app-session.json
Why it matters: the session's own idle and absolute deadlines are the relying party's decision, based on what it protects.
Wait 90 seconds and validate the same ID token again:
btl-lab verifynow fails onexp. Yourapp-session.jsonis still within both deadlines.
Why it matters: the ID token's lifetime limits how long you may accept it as evidence of one sign-in. It says nothing about how long anyone stays signed in.
Still inside fifteen minutes of step 1, ask silently:
signin prompt=none
The listener receives a code with no page in between. Redeem it: the new token has a new iat and the same auth_time as FIRST_AUTH_TIME.
Why it matters: the provider's session is independent of your app's session and of any token's exp.
Wait until more than fifteen minutes have passed since step 1 and run
signin prompt=noneagain. The listener printserror=login_requiredwith yourstateandiss.
Why it matters: single sign-on ends when the provider's session policy says so, and only the provider controls that clock. Your app session in app-session.json is unaffected either way.
Break it
Test the return-page check from the lesson with inputs that must be refused. Resolve each value against your origin and keep it only if the origin is unchanged:
for p in /books/new //other-site.example https://other-site.example/books; do
node -e 'const u=new URL(process.argv[1],"http://127.0.0.1:8765/");console.log(process.argv[1], u.origin==="http://127.0.0.1:8765"?"keep "+u.pathname:"fall back to /")' "$p"
done
/books/new is kept. The other two leave your origin and fall back to the home page. The correct design records the path before the redirect and never reads the destination from the callback, so this check is the second line of defense. Nothing in the tenant was weakened.
Check your work
Press Check my progress. The checks look for the two lifetime changes, Ava's sign-in on the tenant's own page, a code issued with no new sign-in, one UserInfo call, and a login_required answer once the provider session expired.
In your notes: exp - iat = 60, and the silent token's auth_time equals FIRST_AUTH_TIME.
Cleanup
Restore the ID token lifetime and the browser session lifetime you noted in Setup.
Delete
app-session.json.