01Lab Metadata
| Field | Value |
|---|---|
| Lab ID | IB-SIA-12 |
| Track | Identity Bytes, Senior IAM Architect Track |
| Phase | Phase 2, Token Engineering |
| Difficulty | Advanced |
| Estimated Time | 4 to 5 hours |
| Core Technologies | OIDC Session Management, OIDC Back-Channel Logout, refresh token rotation, Keycloak 24.x, Infinispan clustering, Python 3.11, Flask 3.0, curl, jq |
| Builds On | Lab 02 (realm and OIDC clients), Lab 04 (authorization code flow producing the session), Lab 06 (HA cluster and its Infinispan session caches), Lab 09 (reading the logout token JWT) |
| Feeds Into | Lab 33 (identity incident response, where forced session termination is a containment action), Lab 35 (identity observability, which monitors session lifecycle events) |
02Lab Title and Description
Session Management
Tokens are short-lived by design, but the session behind them is not. When Amina Smith logs into Northgate Financial's HR portal, Keycloak creates a user session that outlives any single access token: the portal quietly refreshes its access token in the background using a long-lived refresh token, and the session persists across several applications she uses through single sign-on. This session is where the real security questions of Phase 2 come to rest. What happens when a refresh token is stolen? What happens when Amina, or an administrator, logs her out? And in the two-node cluster you built in Lab 06, where does the session actually live, and what happens to it when a node fails?
This lab answers all three. First you enable refresh token rotation with reuse detection: each use of a refresh token issues a new one and invalidates the old, so that if an attacker steals a refresh token and uses it, the legitimate client's next refresh, or the attacker's second use, is detected as a reuse and the entire session is revoked. Second you configure OIDC Back-Channel Logout so that when a session ends, Keycloak actively notifies every relying party through a signed logout token, rather than leaving stale sessions alive in downstream applications. Third you examine how user session state is held in the Infinispan caches across ib-keycloak-1 and ib-keycloak-2, and confirm that a session created on one node survives the failure of the other.
Getting session management wrong is how "I logged out" becomes "but the other app still had me signed in", and how a stolen refresh token becomes indefinite persistence. This is advanced platform work that a Senior IAM Architect owns end to end.
Estimated completion time: 4 to 5 hours, including rotation configuration, back-channel logout setup, and HA session failover testing.
03Prerequisites
Completed Prior Labs
| Lab | Why it is required |
|---|---|
| IB-SIA-02, Keycloak realm and OIDC federation | Provides the northgate realm and the hr-portal client whose sessions and refresh tokens this lab manages. |
| IB-SIA-04, Authorization code with PKCE | The authorization code flow is what establishes the user session and issues the initial refresh token that this lab rotates. |
| IB-SIA-06, Keycloak High Availability | The Infinispan session caches that hold user and client sessions were configured in Lab 06. This lab inspects and stress-tests that session state under node failure. |
| IB-SIA-09, JWT, JWS and JWE Deep Dive | The OIDC back-channel logout token is a signed JWT with specific claims. You decode and validate it using the skills from Lab 09. |
System Requirements
| Resource | Minimum |
|---|---|
| OS | Ubuntu 22.04 LTS, macOS 13+, or Windows 11 with WSL2 |
| RAM | 8 GB free (the Keycloak HA cluster from Lab 06 accounts for most of this) |
| Disk | 3 GB free |
| CPU | 4 cores recommended for running both nodes plus the relying party |
| Network | Access to the running Keycloak cluster on ib-lab-net; outbound HTTPS to PyPI |
Required Tools
| Tool | Exact Version |
|---|---|
| Docker Engine | 25.0 or later |
| Python | 3.11.x |
| Flask | 3.0.3 |
| requests | 2.32.3 |
| jwcrypto | 1.5.6 (to validate the logout token) |
| curl | 8.x |
| jq | 1.7 |
Install and verify: Ubuntu/Debian
sudo apt update
sudo apt install -y python3.11 python3.11-venv python3-pip jq curl
mkdir -p ~/ib-labs/ib-session
cd ~/ib-labs/ib-session
python3.11 -m venv .venv
source .venv/bin/activate
pip install flask==3.0.3 requests==2.32.3 jwcrypto==1.5.6
python3.11 --version # Expect: Python 3.11.x
python3.11 -c "import flask, requests, jwcrypto; print('libs ok')"
jq --version # Expect: jq-1.7 or later
Install and verify: macOS
brew install python@3.11 jq
mkdir -p ~/ib-labs/ib-session && cd ~/ib-labs/ib-session
python3.11 -m venv .venv && source .venv/bin/activate
pip install flask==3.0.3 requests==2.32.3 jwcrypto==1.5.6
python3.11 -c "import flask, requests, jwcrypto; print('libs ok')"
jq --version
Install and verify: Windows 11 (WSL2)
# Run inside your WSL2 Ubuntu distribution, not PowerShell
wsl --install -d Ubuntu-22.04
# Then follow the Ubuntu/Debian instructions above.
04Real World Problem Statement
A session is a standing grant of access that persists long after any individual token expires. If Northgate cannot detect a stolen refresh token, cannot reliably end a session everywhere at once, and cannot keep sessions alive through a node failure, then its single sign-on becomes a single point of both compromise and outage.
Risk
A stolen refresh token, without rotation, is a long-lived key to Amina's account that renews itself indefinitely. Rotation with reuse detection turns a stolen refresh token from a persistent foothold into a trap: using it trips the reuse detection and revokes the session.
Compliance
The ability to terminate a user's access promptly and completely is required by ISO 27001 Annex A access controls and by FCA operational resilience expectations. A logout that leaves a downstream application still signed in is an incomplete revocation an auditor will note.
Productivity
Reliable single logout means Northgate staff on shared workstations can end their session with confidence that every connected application is also signed out, rather than manually logging out of each one and hoping.
Security Posture
Sessions that survive a node failure mean a routine restart or a node loss does not force thousands of staff to re-authenticate at once, which both protects availability and avoids the support load and credential-fatigue that a mass re-login event causes.
Concrete scenario: During Northgate's last incident review, an analyst asked a simple question no one could answer confidently: if we believe jpatel's refresh token has been stolen, can we be sure that revoking his session actually signs him out of every application, and that the thief's copy of the refresh token stops working? In this lab you build the mechanisms that let you answer yes to both: rotation with reuse detection, and back-channel logout that reaches every relying party.
05Skills Mapped to Production Solutions
| Skill Learned | Real-World Enterprise Application |
|---|---|
| Configuring refresh token rotation with a maximum reuse count and reuse detection | Hardening single sign-on against refresh token theft, a standard requirement in financial services security baselines |
| Implementing an OIDC Back-Channel Logout endpoint that validates the logout token | Building relying parties that participate correctly in single logout across an enterprise application estate |
| Distinguishing online sessions, offline sessions and client sessions | Reasoning about which sessions a logout or revocation actually terminates, a frequent source of production confusion |
| Inspecting and reasoning about session state in Infinispan caches across cluster nodes | Diagnosing session loss, unexpected re-authentication and failover behaviour in clustered identity platforms |
| Forcing administrative session termination as an incident containment action | Executing a rapid, complete access cut-off during incident response, directly feeding Lab 33 |
| Testing session survival through node failure | Validating that an HA identity platform meets its availability objectives under real fault conditions |
06Architecture Overview
Component Breakdown
| Component | Purpose | Technology | Deployment | Ports | Key Configuration |
|---|---|---|---|---|---|
| Keycloak realm session settings | Enforces refresh token rotation, reuse detection and session lifetimes | Keycloak 24.x realm configuration | Existing HA cluster from Lab 06 | 8443 | revokeRefreshToken=true, refreshTokenMaxReuse=0, session idle and max lifespans |
| Infinispan session caches | Hold user sessions, client sessions and offline sessions across nodes | Infinispan distributed cache | ib-keycloak-1 and ib-keycloak-2 from Lab 06 | internal cluster transport | Cache owners of 2 so each entry exists on both nodes |
| Relying party | A demo application that registers a back-channel logout endpoint and terminates its local session on receipt of a valid logout token | Python 3.11, Flask 3.0, jwcrypto | Docker container ib-rp on ib-lab-net | 8098 | Registered Backchannel Logout URL on its Keycloak client |
| PostgreSQL | Stores realm data and, in versions with persistent user sessions, the sessions themselves | PostgreSQL 16 | Existing ib-postgres from Lab 06 | 5432 (internal) | Unchanged from Lab 06 |
Data Flow
- A client refreshes its access token by presenting a refresh token. With rotation enabled, Keycloak returns a new refresh token and marks the presented one as consumed.
Why: a refresh token that is valid only once collapses the window in which a stolen copy is useful, and creates the conditions for reuse detection. - If a consumed refresh token is presented again, Keycloak treats it as a reuse and revokes the entire user session.
Why: a legitimate client never reuses an old refresh token; a second presentation almost always means two parties hold the token, which is exactly the theft scenario, so revoking is the safe response. - When a session ends, for any reason, Keycloak sends a signed logout token to each relying party's registered back-channel logout endpoint. The relying party validates the token's signature and claims, then destroys its local session.
Why: access tokens already issued cannot be un-issued, but the relying party can stop honouring its own session, so single logout is only complete when every relying party is actively told. - Session state is written to the Infinispan caches with two owners, so an entry created while a request was served by one node also exists on the other.
Why: if a node fails, the surviving node already holds the session, so the user is not forced to re-authenticate by the loss of a single node.
Security Considerations
| Concern | Lab Approach |
|---|---|
| Refresh token theft | Rotation with a maximum reuse of zero means any second use of a refresh token is detected and revokes the session. |
| Logout token authenticity | The relying party validates the logout token's signature against the realm JWKS and checks the required events and sid/sub claims before acting. |
| Session fixation and lifetime | Idle and maximum session lifetimes bound how long any session, legitimate or otherwise, can persist. |
| Availability under failure | Two cache owners keep sessions alive through a single node loss, so security-driven restarts do not become an availability incident. |
07Step by Step Implementation
Phase 1: Refresh Token Rotation and Reuse Detection
Step 1.1: Enable rotation and reuse detection on the realm
Set the realm session and refresh settings
# revokeRefreshToken=true turns on rotation; refreshTokenMaxReuse=0 means
# zero permitted reuses, so any second use of a consumed refresh token
# revokes the session. Lifetimes are set to modest lab values.
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh update realms/northgate \
-s revokeRefreshToken=true \
-s refreshTokenMaxReuse=0 \
-s ssoSessionIdleTimeout=1800 \
-s ssoSessionMaxLifespan=36000
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get realms/northgate --fields revokeRefreshToken,refreshTokenMaxReuse,ssoSessionIdleTimeout | jq should show revokeRefreshToken: true and refreshTokenMaxReuse: 0. Because the realm lives in shared PostgreSQL, this change is immediately effective on both nodes.
Step 1.2: Obtain a session and rotate normally
Get an initial token pair and rotate once
# Direct grant used for lab convenience to establish a session.
RESP=$(curl -sk -X POST \
"https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
-d "client_id=hr-portal" \
-d "client_secret=REPLACE_WITH_HR_PORTAL_SECRET" \
-d "grant_type=password" \
-d "username=asmith" \
-d "password=REPLACE_WITH_ASMITH_PASSWORD" \
-d "scope=openid profile")
RT_A=$(echo "$RESP" | jq -r '.refresh_token')
# First refresh: present RT_A, receive RT_B.
RESP2=$(curl -sk -X POST \
"https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
-d "client_id=hr-portal" \
-d "client_secret=REPLACE_WITH_HR_PORTAL_SECRET" \
-d "grant_type=refresh_token" \
-d "refresh_token=$RT_A")
RT_B=$(echo "$RESP2" | jq -r '.refresh_token')
echo "RT_A and RT_B differ: $([ "$RT_A" != "$RT_B" ] && echo yes || echo no)"
RT_A and RT_B differ: yes, confirming rotation is active: the refresh token changed after use. If they are identical, rotation is not enabled; re-check Step 1.1.
Step 1.3: Simulate the theft, reuse the old token, watch the session die
Reuse the consumed token and confirm revocation
# The attacker presents the OLD refresh token RT_A, which was already
# consumed when RT_B was issued. This is the reuse.
echo "=== Attacker reuses consumed RT_A ==="
curl -sk -X POST \
"https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
-d "client_id=hr-portal" \
-d "client_secret=REPLACE_WITH_HR_PORTAL_SECRET" \
-d "grant_type=refresh_token" \
-d "refresh_token=$RT_A" | jq '{error, error_description}'
# Now the LEGITIMATE client tries to use its current, valid RT_B. Because the
# reuse revoked the whole session, even the good token no longer works.
echo "=== Legitimate client tries its current RT_B afterwards ==="
curl -sk -X POST \
"https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
-d "client_id=hr-portal" \
-d "client_secret=REPLACE_WITH_HR_PORTAL_SECRET" \
-d "grant_type=refresh_token" \
-d "refresh_token=$RT_B" | jq '{error, error_description}'
invalid_grant). Critically, the subsequent attempt with the legitimate RT_B also fails, because detecting the reuse revoked the entire session, not merely the reused token. This is the intended behaviour: once two parties are seen holding refresh tokens for one session, the safe action is to end the session and force a fresh, interactive login.
refreshTokenMaxReuse is sometimes set to tolerate benign races, for example a client that retries a refresh after a network timeout and accidentally sends the same token twice. Setting it above zero widens the theft window, so the value must be chosen deliberately, balancing client robustness against security, and documented as a conscious decision.
Phase 2: OIDC Back-Channel Logout
Step 2.1: Build a relying party with a back-channel logout endpoint
rp.py, the relying party with logout validation
# rp.py
# A minimal relying party. It holds a set of "active local sessions" keyed by
# the Keycloak session id (sid). When Keycloak POSTs a back-channel logout
# token, the RP validates it and removes the matching local session.
import json
import time
import requests
from flask import Flask, request, jsonify
from jwcrypto import jwt, jwk
app = Flask(__name__)
ISSUER = "https://ib-lb:8443/realms/northgate"
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"
CLIENT_ID = "hr-portal"
requests.packages.urllib3.disable_warnings()
_jwks = jwk.JWKSet.from_json(requests.get(JWKS_URL, verify=False).text)
# Demo local session store: sid -> username. Populated at login in a real RP;
# seeded here via /debug/seed for the lab.
LOCAL_SESSIONS = {}
@app.post("/debug/seed")
def seed():
body = request.get_json(force=True)
LOCAL_SESSIONS[body["sid"]] = body["username"]
return jsonify({"active_sessions": LOCAL_SESSIONS})
@app.post("/backchannel-logout")
def backchannel_logout():
logout_token = request.form.get("logout_token", "")
try:
verified = jwt.JWT(jwt=logout_token, key=_jwks, algs=["RS256"])
claims = json.loads(verified.claims)
except Exception as e:
return jsonify({"error": f"invalid logout token: {e}"}), 400
# Required checks per the OIDC Back-Channel Logout specification.
if claims.get("iss") != ISSUER:
return jsonify({"error": "issuer mismatch"}), 400
aud = claims.get("aud")
aud_list = aud if isinstance(aud, list) else [aud]
if CLIENT_ID not in aud_list:
return jsonify({"error": "audience mismatch"}), 400
events = claims.get("events", {})
if "http://schemas.openid.net/event/backchannel-logout" not in events:
return jsonify({"error": "not a back-channel logout event"}), 400
# A logout token must NOT contain a nonce, and must contain sub or sid.
if "nonce" in claims:
return jsonify({"error": "logout token must not contain nonce"}), 400
sid = claims.get("sid")
removed = LOCAL_SESSIONS.pop(sid, None) if sid else None
return jsonify({"terminated_sid": sid, "was_active": removed is not None,
"remaining": LOCAL_SESSIONS}), 200
@app.get("/sessions")
def sessions():
return jsonify(LOCAL_SESSIONS)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8098, ssl_context="adhoc")
Run the relying party
export FLASK_ENV=development
python3.11 rp.py # listens on https://localhost:8098
curl -sk https://localhost:8098/sessions | jq returns an empty object. If it fails to start, confirm the JWKS fetch succeeded, which requires the Keycloak cluster to be reachable at ib-lb:8443.
Step 2.2: Register the back-channel logout URL on the client
Set the logout URL and session-required flag
# Replace HR_PORTAL_UUID with the hr-portal client UUID from Lab 02.
# From the host, ib-rp is reachable on the Docker network; use the container
# name if the RP runs as a container, or host.docker.internal if it runs on
# the host and Keycloak runs in Docker. Adjust to your setup.
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh update \
clients/HR_PORTAL_UUID -r northgate \
-s 'attributes."backchannel.logout.url"=https://ib-rp:8098/backchannel-logout' \
-s 'attributes."backchannel.logout.session.required"=true'
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get clients/HR_PORTAL_UUID -r northgate | jq '.attributes | {url: ."backchannel.logout.url", sess: ."backchannel.logout.session.required"}' should show your URL and "true". If the URL is unreachable from Keycloak's network at logout time, the logout token delivery will fail silently from the user's perspective; Section 8 covers diagnosing this.
Step 2.3: Trigger a logout and observe the relying party session end
Seed a local RP session, then force logout and observe
# Establish a Keycloak session and capture its sid from the ID token.
RESP=$(curl -sk -X POST \
"https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
-d "client_id=hr-portal" -d "client_secret=REPLACE_WITH_HR_PORTAL_SECRET" \
-d "grant_type=password" -d "username=asmith" \
-d "password=REPLACE_WITH_ASMITH_PASSWORD" -d "scope=openid profile")
ID_TOKEN=$(echo "$RESP" | jq -r '.id_token')
SID=$(echo "$ID_TOKEN" | cut -d. -f2 | tr '_-' '/+' | \
awk '{l=length($0)%4; if(l>0)for(i=0;i<4-l;i++)$0=$0"="; print}' | \
base64 -d 2>/dev/null | jq -r '.sid')
# Mirror that session into the RP's local store.
curl -sk -X POST https://localhost:8098/debug/seed \
-H "Content-Type: application/json" \
-d "{\"sid\":\"$SID\",\"username\":\"asmith\"}" | jq
# Now force-logout the user's sessions as an administrator. This is also the
# exact containment action used in Lab 33.
USER_ID=$(docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get \
users -r northgate -q username=asmith --fields id | jq -r '.[0].id')
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh create \
users/$USER_ID/logout -r northgate
# The RP should have received a logout token and dropped the session.
sleep 2
curl -sk https://localhost:8098/sessions | jq
/sessions on the RP should no longer contain the seeded sid, proving Keycloak actively delivered a valid logout token and the RP terminated its local session. If the session is still present, check that Keycloak can reach the RP's URL and that the RP's validation of iss, aud and events matched the token; the RP logs will show which check failed.
logout_token, so it works even when the user's browser is closed. This is its advantage over front-channel logout, which relies on loading logout URLs in the user's browser and fails if the browser is gone.
Phase 3: Session State Across the HA Cluster
Step 3.1: Inspect the session caches on both nodes
Query active sessions and cache ownership
# Count active sessions for the hr-portal client via the Admin REST API.
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get \
clients/HR_PORTAL_UUID/session-count -r northgate
# Confirm the cluster sees both nodes and the session caches are distributed.
# The exact cache names are 'sessions', 'clientSessions', 'offlineSessions'
# and 'offlineClientSessions'. Cache metrics are exposed on the management
# port if enabled in Lab 06.
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get \
serverinfo -r master | jq '.systemInfo.serverTime, .memoryInfo.total'
sessions cache reports entries on both nodes, which is the evidence that session state is replicated rather than pinned to one node.
Step 3.2: Prove a session survives a node failure
Establish a session, kill a node, verify the session persists
# Establish a fresh session and keep its refresh token.
RESP=$(curl -sk -X POST \
"https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
-d "client_id=hr-portal" -d "client_secret=REPLACE_WITH_HR_PORTAL_SECRET" \
-d "grant_type=password" -d "username=jpatel" \
-d "password=REPLACE_WITH_JPATEL_PASSWORD" -d "scope=openid profile")
RT=$(echo "$RESP" | jq -r '.refresh_token')
# Simulate the failure of one node.
docker stop ib-keycloak-1
# The load balancer routes the next refresh to ib-keycloak-2, which must
# already hold the session because the caches have two owners.
sleep 3
curl -sk -X POST \
"https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
-d "client_id=hr-portal" -d "client_secret=REPLACE_WITH_HR_PORTAL_SECRET" \
-d "grant_type=refresh_token" -d "refresh_token=$RT" \
| jq '{ok: (.access_token != null), error}'
# Restore the node for later labs.
docker start ib-keycloak-1
ib-keycloak-1 should return ok: true, proving the session created earlier survived on ib-keycloak-2. If it returns an error indicating the session is unknown, the session caches are not configured with two owners, or persistent sessions are not enabled; revisit the Infinispan configuration from Lab 06 before relying on failover.
docker start ib-keycloak-1 afterwards. Leaving the cluster running on a single node removes the redundancy every subsequent lab assumes, and a second node loss would then take the whole identity platform down.
08Testing and Validation
End-to-End Test Scenarios
| Scenario | Steps | Expected Result |
|---|---|---|
| Normal rotation | Refresh once and compare tokens | A new refresh token is returned; the old one differs |
| Reuse revocation | Reuse a consumed refresh token, then try the current one | Both fail; the session was revoked on reuse detection |
| Back-channel logout | Seed an RP session, force admin logout | The RP receives a valid logout token and drops the session |
| Node failure survival | Establish a session, stop one node, refresh | The refresh succeeds via the surviving node |
Negative Tests
| Test | Expected Result |
|---|---|
| POST a logout token with a wrong issuer to the RP | HTTP 400, issuer mismatch, no session terminated |
| POST a logout token whose signature does not verify | HTTP 400, invalid logout token |
POST a token containing a nonce claim to the RP logout endpoint | HTTP 400; a logout token must not contain a nonce |
| Refresh after the SSO idle timeout has elapsed | invalid_grant; the session expired through inactivity |
Common Failure Modes
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Refresh token does not change on refresh | revokeRefreshToken is false | Re-apply Step 1.1 and confirm with the realm fields query |
| Reuse does not revoke the session | refreshTokenMaxReuse is greater than zero | Set it to 0 for strict single use, then retest |
| Back-channel logout never reaches the RP | Keycloak cannot resolve or reach the RP URL from its own network | Confirm the URL is reachable from inside the Keycloak container, adjusting host naming (ib-rp vs host.docker.internal) to your topology |
| Session lost when a node stops | Session caches have a single owner, or persistent sessions are off in a version that needs them | Set cache owners to 2 and confirm the persistent session behaviour for your version |
09Security Analysis
What Makes This Implementation Secure
- Refresh token rotation with zero permitted reuse means a stolen refresh token cannot be used more than once without triggering full session revocation.
- Reuse detection revokes the entire session rather than only the reused token, correctly treating a detected reuse as evidence of compromise.
- The relying party validates the logout token's signature, issuer, audience and event type before acting, so a forged or misdirected token cannot terminate sessions.
- Back-channel logout works server to server, so a session can be ended even when the user's browser is gone, unlike front-channel logout.
- Two cache owners keep sessions available through a single node failure, so a security-driven restart does not become an availability outage.
What Is Intentionally Simplified for the Lab
- The direct grant establishes sessions quickly; production sessions come from the authorization code with PKCE flow from Lab 04.
- The relying party's local session store is an in-memory dictionary seeded via a debug endpoint; a real RP populates it at login and persists it appropriately.
- TLS certificate verification is disabled against
ib-lband the local RP because they present self-signed certificates. - The lab tests failover by stopping one node manually; a production validation would include repeated failovers and sustained load during the failure.
Production Hardening Recommendations
| Area | Recommendation |
|---|---|
| Reuse tolerance | Keep refreshTokenMaxReuse at 0 unless a specific client robustness need justifies otherwise, and document any non-zero value as a conscious risk decision |
| Logout coverage | Register back-channel logout URLs on every relying party, and monitor for delivery failures so a silently unreachable RP does not leave stale sessions |
| Session lifetimes | Set idle and maximum session lifespans to match the sensitivity of the applications; shorter for high-risk, and align offline session lifetimes deliberately |
| Failover testing | Include node-failure and rolling-restart drills in regular resilience testing, confirming sessions survive and logout still propagates during a failover |
| Monitoring | Alert on spikes in reuse-detection revocations, which can indicate either a token theft campaign or a misbehaving client, feeding Lab 35's observability work |
10Cleanup
Stop the relying party and confirm the cluster is whole
cd ~/ib-labs/ib-session
# Stop the RP terminal with Ctrl+C.
# CRITICAL: confirm both Keycloak nodes are running after the Phase 3 test.
docker ps --filter "name=ib-keycloak" --format '{{.Names}}: {{.Status}}'
# If ib-keycloak-1 is not listed as Up, start it:
docker start ib-keycloak-1
deactivate 2>/dev/null || true
Optionally revert the back-channel logout URL and relax lab lifetimes
# Keep the rotation settings, they are a genuine improvement worth retaining.
# Only revert the back-channel URL if you are removing the demo RP entirely.
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh update \
clients/HR_PORTAL_UUID -r northgate \
-s 'attributes."backchannel.logout.url"='
ib-keycloak-1 and ib-keycloak-2 report Up and healthy, and that the RP process is stopped. Retaining refresh token rotation is recommended; the cluster is otherwise ready for Phase 3 and Lab 13.
11Recommended Learning Links
- OpenID Connect Back-Channel Logout 1.0, OpenID Foundation
- OpenID Connect Front-Channel Logout 1.0, OpenID Foundation
- OpenID Connect Session Management 1.0, OpenID Foundation
- OAuth 2.0 Security Best Current Practice, refresh token rotation section, IETF
- Keycloak Server Administration Guide, Sessions and Logout, Keycloak documentation
- Keycloak documentation on configuring distributed caches and persistent user sessions, Keycloak documentation
12Portfolio Publishing Guide
Sanitise Before Publishing
This lab handles refresh tokens and a client secret. Confirm none reach your repository.
Sanitisation checklist commands
grep -R "REPLACE_WITH" . --include="*.py" --include="*.sh" --include="*.md" || echo "Clean"
cat >> .gitignore <<'EOF'
*.token
.env
.venv/
EOF
git status
README for the Repository
README.md skeleton
# IB-SIA-12: Session Management
Demonstrates refresh token rotation with reuse detection, an OIDC
Back-Channel Logout relying party, and session survival across a Keycloak
HA cluster node failure.
## Stack
Python 3.11, Flask 3.0, jwcrypto 1.5, Keycloak 24.x (Infinispan clustering)
## What it demonstrates
- Refresh rotation: a reused token revokes the whole session
- Back-channel logout: a signed logout token terminates a relying party session
- HA: a session survives the loss of one cluster node
## Part of the Identity Bytes Senior IAM Architect Track
Lab 12 of 36. See identity-bytes.com for the full curriculum.
Git Commands
Commit and push
git add rp.py README.md .gitignore
git commit -m "IB-SIA-12: session management, refresh rotation, back-channel logout, HA failover"
git push origin main
Track Index Line
Add the following line to your master portfolio index:
IB-SIA-12 | Session Management | Advanced | Refresh rotation, reuse detection, back-channel logout, HA session failover
LinkedIn Draft
"I logged out." The other application: "Never heard of it, you are still signed in here."
Tokens expire in minutes. The session behind them can last all day, and that session is where the real security questions live. This week I worked through three of them against a clustered Keycloak.
First, refresh token theft. With rotation and reuse detection, a stolen refresh token stops being a self-renewing foothold and becomes a tripwire: the moment it is used a second time, the whole session is revoked and everyone re-authenticates. Yes, that inconveniences the legitimate user too. That is the correct trade, because the alternative is quietly tolerating an attacker who might hold a working token.
Second, single logout that is actually single. OIDC Back-Channel Logout delivers a signed logout token server to server to every relying party, so logging out means logging out everywhere, even if the browser is already closed.
Third, the availability side: I stopped a cluster node mid-session and confirmed the user stayed logged in, because the session already existed on the surviving node. A security restart should never become an outage.
If you asked your platform today, "are we certain revoking this user's session signs them out of every connected app and kills the thief's token," could you answer yes?