IDENTITY FUNDAMENTALS · LAB
Watch a session start, change identifier and end
Read your tenant's real session cookie, prove its identifier is replaced when sign-in completes, show that clearing a cookie is not signing out, and end a session on the server.
Partly readyUses your lab tenant
The lesson
Builds on: How applications communicate.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.
- G42 Session idle timeout (only absolute lifetime today)
- G30 Admin session listing and kill-session
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 browser session lifetime
Recorded as
tenant.oauth.policy.updatesucceeded.Finish Ava's sign-in with her second step
Recorded as
account.second_stepsucceeded (second_step_completed) about[email protected].Get a code for the printer using only the session cookie
Recorded as
oauth.authorizesucceeded (code_issued) forlab-printerabout[email protected].End the session on the server
Recorded as
account.sign_outsucceeded (signed_out).Change Ava's password as an administrator
Recorded as
tenant.users.credentials.setsucceeded about[email protected].
Setup
You need Ava with her authenticator app and no management roles, lab-printer with Ava's consent, and the toolkit.
On this lab page choose Lab Photos and press Start.
Flow policy: write down the current browser session lifetime, then set it to
900seconds. Save.In the shell, set the variables and helpers from the How applications communicate lab:
ISSUER=https://tenant-<id>.beyondthelogin.dev
CLIENT_ID=<lab-printer client ID>
REDIRECT=http://127.0.0.1:8765/callback
authz() { echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(node -p 'encodeURIComponent(process.argv[1])' "$REDIRECT")&scope=$1&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"; }
fresh() { eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; }
check() { for s in "$@"; do curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -H "Cookie: __Host-btl-oauth-session=$s" "$ISSUER/account"; done; }
Note: you will copy Ava's session identifiers into the shell. They belong to a test user, never leave your machine, and stop working by the end of the lab.
Walkthrough
Open a private window and DevTools > Application > Cookies for your tenant host. Open
$ISSUER/login. There is a__Host-btl-signincookie, which binds email codes and links to this browser, but no session cookie yet.Enter Ava's email and password. On the authenticator page a
__Host-btl-oauth-sessioncookie appears. Copy its value:
read -rs STAGED
Enter the authenticator code. The
__Host-btl-oauth-sessionvalue is now different. Copy it too, and read its attributes in DevTools:Path=/,Secure,HttpOnly,SameSite=Lax, an expiry about 15 minutes away, and no Domain.
read -rs SESSION
Why it matters: these are the attributes from the lesson's Set-Cookie example. The __Host- prefix keeps the cookie on this exact host.
Compare the two identifiers:
check "$STAGED" "$SESSION"
Expected: 303 to /login?continue=%2Faccount for the staged value and 200 for the new one.
Why it matters: the identifier that existed before sign-in completed no longer works. That is the defense against session fixation: a new identifier on authentication, and the old one invalidated on the server, not just replaced in the browser.
Connect a separate request to the earlier sign-in:
fresh; curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -H "Cookie: __Host-btl-oauth-session=$SESSION" "$(authz openid)&prompt=none"
Expected: 303 to your redirect URI with a code, and no password asked.
Why it matters: the cookie, not a new password, tells the server that this request belongs to Ava's earlier sign-in.
In the DevTools Console run
document.cookie. The session cookie is not listed.
Why it matters: HttpOnly keeps page scripts from reading the session identifier.
In DevTools, delete the
__Host-btl-oauth-sessioncookie and reload$ISSUER/account: the sign-in page appears. Now runcheck "$SESSION": still200.
Why it matters: clearing the browser's copy did not end the session. A stolen copy would still work.
End it on the server. This is the request the Sign out button on
/accountsends:
curl -si -X POST "$ISSUER/logout" -H "Origin: $ISSUER" -H "Cookie: __Host-btl-oauth-session=$SESSION" | grep -iE '^(HTTP|location|set-cookie)'
check "$SESSION"
Expected: 303 to /login with set-cookie: __Host-btl-oauth-session=; ... Max-Age=0, and then 303 to sign-in for the old value. Audit shows account.sign_out succeeded with reason signed_out.
Why it matters: signing out invalidated the server's session record as well as removing the browser's cookie.
Absolute lifetime: sign in again and either keep using
/accountor leave the tab alone. Either way, 15 minutes after sign-in,/accountsends you to sign in. If you do not want to wait, read the step and move on.
Why it matters: this setting is an absolute timeout counted from sign-in. Activity does not extend it. The lesson's idle timeout is not available as a setting yet (G42).
Sessions ended by the service: sign in as Ava again and copy the new cookie with
read -rs SESSION. As yourself, choose Set password on Ava's record and give her a new password. Runcheck "$SESSION":303to sign-in.
Why it matters: the service, not the browser, decides whether a session is still valid. A password change ends Ava's sessions and revokes her tokens.
Planned walkthrough
These steps need an idle timeout setting (G42) and an administrator session list (G30).
Flow policy: set the idle timeout to 5 minutes and the absolute lifetime to 60 minutes.
Sign in as Ava in two private windows. Keep using one; leave the other alone. After 5 minutes the idle window is sent to sign in while the active one continues. After 60 minutes both end.
On Ava's record open Sessions. Each session shows when it was created, when it was last used and which browser holds it. End one session without changing her password; only that browser is signed out, and Audit records who ended it.
Break it
Sign in again, copy the cookie with
read -rs SESSION, and send the sign-out request from step 8 without theOriginheader. The response still redirects to/login, butcheck "$SESSION"shows the session is valid.
Why it matters: the server refuses state-changing requests that did not come from its own pages, so another site cannot sign Ava out or act with her cookie.
Check your work
Press Check my progress. It looks for:
tenant.oauth.policy.updatesucceeded (the shorter lifetime).account.second_stepsucceeded with reasonsecond_step_completedfor Ava.oauth.authorizesucceeded with reasoncode_issuedforlab-printerand Ava, with no sign-in in between (step 5).account.sign_outsucceeded with reasonsigned_out.tenant.users.credentials.setsucceeded for Ava.
Cleanup
Flow policy: restore the browser session lifetime you wrote down.
Store Ava's new password in your password manager and run
unset STAGED SESSION.
Missing infrastructure
G42 Session idle timeout. The tenant has only an absolute browser session lifetime. With an idle setting, the planned steps show an idle session ending while an active one survives, and the absolute cap ending both.
G30 Administrator session listing and kill-session. There is no view of a user's active sessions and no way to end one session. Today step 10 ends all of Ava's sessions with a password change instead.