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

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.

  1. Sign in again because the session was older than max_age

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

  2. Reuse a recent enough sign-in with no page

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

  3. Force a sign-in with max_age=0

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

  4. See the tenant refuse a sign-in by someone else

    Recorded as oauth.authorize rejected (id_token_hint_mismatch) for lab-collage.

Setup

  1. Sign Ava in to Lab Photos in your lab browser, then wait at least five minutes before step 2. Keep ID_TOKEN_AVA from Key accounts on issuer and subject.

  2. 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"
}
  1. Run btl-lab callback before each request and press Start.

Walkthrough

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

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

  1. Within five minutes, repeat step 2. No page appears, and auth_time equals the one from step 2. recent prints OK again.

Why it matters: a recent enough sign-in is reused, so a second change a minute later needs no extra trip.

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

  1. Confirm the tenant includes auth_time even without max_age: run a plain signin, redeem, and read part "$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

  1. 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 plain signin with no max_age as the address change's sign-in. The tenant answers silently from the old session, and recent prints FAIL sign-in older than 300 seconds. Only the relying party's own check stops the change.

  2. Someone else signs in. Run signin max_age=0 without the hint and sign in as Ben: recent prints FAIL 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.

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

  1. TOLERANCE=30, SCOPE="openid profile email".

  2. Sign Ben out and sign Ava back in. Nothing in the tenant changed.

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