OPENID CONNECT · LAB
Demand a recent sign-in with max_age and check auth_time yourself
Protect a pending delivery address change with a five-minute rule, ask for it with max_age and id_token_hint, and make the relying party's own auth_time and subject checks catch what the provider never saw.
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.
Sign in again because the session was older than max_age
Recorded as
oauth.authorizesucceeded (user_signed_in) forlab-collageabout[email protected].Reuse a recent enough sign-in with no page
Recorded as
oauth.authorizesucceeded (code_issued) forlab-collageabout[email protected].Force a sign-in with max_age=0
Recorded as
oauth.authorizesucceeded (user_signed_in) forlab-collageabout[email protected].See the tenant refuse a sign-in by someone else
Recorded as
oauth.authorizerejected (id_token_hint_mismatch) forlab-collage.
Setup
Sign Ava in to Lab Photos in your lab browser, then wait at least five minutes before step 2. Keep
ID_TOKEN_AVAfrom Key accounts on issuer and subject.Load the helpers, ask only for a sign-in, and add the relying party's check. It runs after the usual validation and compares the subject first, then
auth_time, with a tolerance of seconds.
source ~/btl-oidc.sh
SCOPE="openid"; MAX_AGE=300; TOLERANCE=30
EXPECTED_SUB=$(part "$ID_TOKEN_AVA" | jq -r .sub)
recent() {
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE" > /dev/null || { echo "FAIL validation"; return 1; }
local c t; c=$(part "$ID_TOKEN")
[ "$(jq -r .sub <<<"$c")" = "$EXPECTED_SUB" ] || { echo "FAIL other subject: discard the change, end the session"; return 1; }
t=$(jq -r '.auth_time // empty' <<<"$c"); [ -n "$t" ] || { echo "FAIL auth_time missing"; return 1; }
[ $(( $(date +%s) - t )) -le $(( MAX_AGE + TOLERANCE )) ] || { echo "FAIL sign-in older than $MAX_AGE seconds"; return 1; }
echo "OK apply the pending address change"
}
Run
btl-lab callbackbefore each request and press Start.
Walkthrough
Note how old the sign-in behind your session is. Ask silently and read
auth_time:
signin prompt=none
redeem '<code>'
part "$ID_TOKEN" | jq '{auth_time, age_seconds: (now - .auth_time | floor)}'
Why it matters: a session can be young while the sign-in behind it is old. Nothing the relying party does on its own moves auth_time forward.
Hold the new address as a pending change and ask for a recent sign-in by the same person:
signin max_age=300 "id_token_hint=$ID_TOKEN_AVA"
The tenant shows its password form. Sign in as Ava, redeem, then run recent: OK apply the pending address change.
Why it matters: max_age asks for exactly the freshness the action needs, and the hint makes sure the fresh sign-in is for the account this session belongs to.
Within five minutes, repeat step 2. No page appears, and
auth_timeequals the one from step 2.recentprints OK again.
Why it matters: a recent enough sign-in is reused, so a second change a minute later needs no extra trip.
Demand a sign-in every time:
signin max_age=0
The form appears even though Ava signed in a minute ago, and the new auth_time is now.
Why it matters: max_age=0 is the reliable equivalent of prompt=login. Any request with max_age must receive auth_time.
Confirm the tenant includes
auth_timeeven withoutmax_age: run a plainsignin, redeem, and readpart "$ID_TOKEN" | jq .auth_time.
Why it matters: the lesson's require_auth_time registration setting gives this guarantee per client. Here it is the tenant's default for every ID token, so your session records always have a real value.
Note: clients have no default_max_age setting yet (G60), and the tenant does not support the claims parameter (G19). With G60, you would set a maximum age on lab-collage and see a request's own max_age override it.
Break it
The request that arrives without
max_age. The request crosses the browser, so the provider may never see the parameter. Wait more than five minutes, then use a plainsigninwith nomax_ageas the address change's sign-in. The tenant answers silently from the old session, andrecentprintsFAIL sign-in older than 300 seconds. Only the relying party's own check stops the change.Someone else signs in. Run
signin max_age=0without the hint and sign in as Ben:recentprintsFAIL other subject. Now add the hint and try Ben again:
signin max_age=0 "id_token_hint=$ID_TOKEN_AVA"
After Ben enters his password, the listener prints error=login_required. The tenant itself refused to return a sign-in for anyone other than the hinted person.
Widen the tolerance to an hour (
TOLERANCE=3600) and rerun Break it 1: the stale sign-in passes. A clock tolerance is seconds, never enough to turn five minutes into an hour.
Restore: set TOLERANCE=30.
Check your work
Press Check my progress. The checks look for a real password sign-in caused by max_age, a code issued from a recent enough session, a forced sign-in with max_age=0, and the tenant's refusal when Ben signed in under Ava's hint.
In Audit, step 3's request has request_started and code_issued with no user_signed_in between them. That gap is how you tell a reused sign-in from a new one.
Cleanup
TOLERANCE=30,SCOPE="openid profile email".Sign Ben out and sign Ava back in. Nothing in the tenant changed.