OPENID CONNECT · LAB
Logout tokens and an administrator-ended session
Plan validating a real logout token when an administrator locks a user. Today, prove an ID token fails the logout token rules, then lock a user and see which tokens die and which sessions never hear.
PlannedUses your lab tenant
The lesson
Builds on: Local logout and provider sessions.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Planned. The core of this lab waits on platform features that are not built yet. The planned walkthrough shows exactly how it will run; Do today is a real exercise you can do now.
- G17 OIDC logout: RP-initiated, front-channel, back-channel, session management
- G30 Admin session listing and kill-session
- G4 Hosted lab callback page / demo relying party beyond Token Decoder
Setup
Your tenant has no backchannel_logout_uri, logout tokens, or delivery with retries yet (G17), and no administrator view to end one user's sessions without changing the account (G30). The provider side of "end everything for a leaver" is real today: locking a user.
source ~/btl-oidc.shand runbtl-lab callbackbefore each sign-in.Once G17 exists: on
lab-collage, register a back-channel logout URI on an HTTPS address the tenant can reach, such as a hosted lab receiver (G4) or your own tunnel, with "session required" off.
Planned walkthrough
Sign Ava in to
lab-collageand to a second app such aslab-tmp-slideshow.In Users, lock Ava. The tenant posts
logout_token=...to each registered client's back-channel logout URI.Your receiver validates the token: the signature by
kidfrom the JWKS with an algorithm you accept for ID tokens,typlogout+jwt,iss,aud,iatandexp(about two minutes),suborsidpresent, the back-channel logout member inevents, nononce, and ajtiit has not seen. It ends every session for thatissandsuband answers200withCache-Control: no-store.
Why it matters: the logout address is reachable by anyone who can reach the app, so only a validated signature makes the message safe to act on.
Make the receiver answer
503once. Logs shows a failed delivery and a later retry carrying a newly issued token.Make it answer
400. The tenant records the failure and does not retry.
Why it matters: retries are for failures that look temporary, and a retry minutes later needs a new token because the first one expired.
Do today
Write the logout token rules. The signature,
iss,audand times are checked exactly as for an ID token; these are the rules that keep the two kinds of token apart:
logout_rules() {
jq -rn --argjson h "$(part "$1" 1)" --argjson p "$(part "$1")" '
[ (if $h.typ != "logout+jwt" then "typ is \($h.typ // "missing")" else empty end),
(if ($p.sub == null and $p.sid == null) then "no sub or sid" else empty end),
(if ($p.events["http://schemas.openid.net/event/backchannel-logout"] | type) != "object" then "no back-channel logout event" else empty end),
(if $p.nonce != null then "nonce present" else empty end) ]
| if length == 0 then "accept" else "reject: " + join("; ") end'
}
Feed it a real ID token. Sign Ava in to
lab-collage, validate the ID token withbtl-lab verify(it passes), then runlogout_rules "$ID_TOKEN":reject: typ is JWT; no back-channel logout event; nonce present.
Why it matters: an ID token must never be accepted as a logout token. The nonce rule guarantees it from both sides, because a logout token slipped into a sign-in fails the nonce check too.
Prepare for an administrator ending Ava's access. Sign her in to
lab-collagewith offline access and keep all three tokens:
SCOPE="openid photos.read offline_access"
signin prompt=consent
redeem '<code>'
AT=$TOKEN RT=$REFRESH
In Users, lock Ava. Then check each piece:
signin prompt=none
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=refresh_token --data-urlencode "refresh_token=$RT" | jq .error
curl -si -H "Authorization: Bearer $AT" "$ISSUER/oidc/userinfo" | grep -i '^www-authenticate'
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/introspect" --data-urlencode "token=$AT" | jq .active
btl-lab verify "$AT" --issuer "$ISSUER" --audience "$ISSUER/resource" --type at+jwt
The silent check returns login_required, the refresh invalid_grant, UserInfo Bearer error="invalid_token", and introspection false. Local verification of the same JWT access token still passes: its signature is good and exp has not passed. Your own app's local session, and any other app's, are untouched.
Why it matters: the provider ended everything it controls. A resource server that validates JWTs locally does not notice, and relying parties' sessions run on until a logout message, which is the missing piece, or their own timeouts.
Unlock Ava in Users. Her old tokens stay dead; she signs in again.
Break it
Planned, once G17 exists: let the tenant's retry deliver a token your receiver already processed, and see it rejected on jti; process one after its two minutes, and see it rejected on exp.
Check your work
Today: Audit shows tenant.users.lock and tenant.users.unlock, then oidc.userinfo and oauth.token rejections for lab-collage. Your notes record which checks still passed after the lock.
Once G17 exists, Logs shows each delivery per client with its outcome, and your receiver's log keeps the jti, the result and the number of sessions ended, never the token.
Cleanup
Make sure Ava is unlocked, and set
SCOPE="openid profile email".Once G17 exists, remove the back-channel logout URI.
Missing infrastructure
G17 (OIDC logout). Back-channel logout registration, logout tokens (
typlogout+jwt,events,sidorsub,jti, shortexp), delivery with bounded retries, and delivery events in Logs. The toolkit'sbtl-lab verifywould also gain a logout token type.G30 (admin session listing and kill-session). Listing and ending a user's sessions from Users without locking the account.
G4 (hosted lab receiver). The tenant cannot reach
127.0.0.1, so learners without public HTTPS need a hosted receiver to see deliveries.