<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Session Management on Matt Goodrich</title><link>https://mattgoodrich.com/tags/session-management/</link><description>Recent content in Session Management on Matt Goodrich</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 18 Jun 2026 00:01:00 -0700</lastBuildDate><atom:link href="https://mattgoodrich.com/tags/session-management/index.xml" rel="self" type="application/rss+xml"/><item><title>Logging Out Is Harder Than Logging In</title><link>https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/</link><pubDate>Thu, 18 Jun 2026 00:01:00 -0700</pubDate><guid>https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/</guid><description>&lt;img src="https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/header.png" alt="Featured image of post Logging Out Is Harder Than Logging In" />&lt;p>Single sign-on solved logging in. One identity provider, one handshake, and every app trusts the result. Logging out never got the same treatment, and the reason is structural: there was never just one session to end.&lt;/p>
&lt;p>When you log into an app through SSO, the identity provider proves who you are once, and then the app sets up its own session and stops asking. Killing your account at the IdP does nothing to that session, because the app was never checking back. This is why a disabled employee can keep working in a tool for days: the &lt;a class="link" href="https://mattgoodrich.com/posts/automate-the-leaver-before-the-joiner/" >account is revoked but the session is not&lt;/a>, and the session is the thing actually serving the requests.&lt;/p>
&lt;h2 id="there-is-never-just-one-session">There Is Never Just One Session
&lt;/h2>&lt;p>Count the sessions behind a single SSO login and there are at least two, usually more.&lt;/p>
&lt;p>There is the &lt;strong>IdP session&lt;/strong>, your authenticated session at the identity provider, the thing that lets you open a second app without logging in again. There is the &lt;strong>application session&lt;/strong> at each service provider, created the moment the IdP vouches for you, living in that app&amp;rsquo;s own cookie or token and governed by that app&amp;rsquo;s own rules. Log into five apps through SSO and you have one IdP session and five app sessions: six independent timers, each set by a different team with different defaults.&lt;/p>
&lt;p>Under those sit the token lifetimes. An OIDC access token might be good for an hour, while the refresh token behind it is good for weeks, quietly minting new access tokens the whole time. The app session, the access token, and the refresh token are three more clocks, and they do not run together.&lt;/p>
&lt;p>Put numbers on it and the problem is plain. A common setup is an eight-hour sliding app session, a one-hour access token, a thirty-day refresh token, and an eight-hour IdP session. Disable the account at noon and the access token keeps working until one o&amp;rsquo;clock, the refresh token can mint fresh ones for a month, and the sliding app session renews for as long as the tab stays open. The single logout you wanted is four different expiries owned by three different systems, and none of them fired when you clicked disable.&lt;/p>
&lt;p>&lt;img src="https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/diagram-sessions.png"
width="1568"
height="172"
srcset="https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/diagram-sessions_hu15914487413573580515.png 480w, https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/diagram-sessions_hu5507730992671022375.png 1024w"
loading="lazy"
alt="One SSO Login Creates Several Independent Sessions and Tokens, Each on Its Own Clock: an IdP Session, a Sliding App Session, a Short Access Token, and a Long Refresh Token; Disabling the Account Blocks the Next Login but Leaves the Access Token Valid Until It Expires, the Refresh Token Minting New Ones for Weeks, and the Sliding App Session Alive Until the Tab Closes"
class="gallery-image"
data-flex-grow="911"
data-flex-basis="2187px"
>&lt;/p>
&lt;p>The load-bearing word is independent. SSO is a one-time handshake, not a standing connection. Once the app has its session it has no reason to phone the IdP on every request, and for performance it deliberately does not. The decision that makes SSO fast is the same decision that makes logout hard. There is no single session to end, and no single place that knows about all of them.&lt;/p>
&lt;h2 id="fixed-vs-sliding-sessions">Fixed vs Sliding Sessions
&lt;/h2>&lt;p>Sessions also differ in how they expire, and that difference decides how long a stranded one lives.&lt;/p>
&lt;p>A &lt;strong>fixed&lt;/strong> or absolute session ends a set time after login whether you are active or not: eight hours, then back to the login screen. A &lt;strong>sliding&lt;/strong> session, also called rolling, resets its timer on every bit of activity, so a user who keeps working never gets logged out. Most apps default to sliding, because logging active users out feels broken to them.&lt;/p>
&lt;p>Sliding is where access quietly becomes immortal. As long as the session stays active enough, a background tab polling, a mobile app checking for messages, it renews itself indefinitely, and &amp;ldquo;active enough&amp;rdquo; is a low bar. A disabled account whose app session is sliding does not expire on a schedule. It expires when the person finally closes the tab, which can be never. The &lt;a class="link" href="https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html" target="_blank" rel="noopener"
>OWASP session management guidance&lt;/a> is to pair an idle timeout with a hard absolute timeout for exactly this reason, so a sliding session has a ceiling it cannot renew past. Most apps ship the convenient default and skip the absolute cap.&lt;/p>
&lt;p>A tight session on one end does not bound the other. You can set a strict one-hour IdP session and feel covered, but if the app opened a long sliding session of its own at login, the IdP timeout never touches it. Exposure is set by the loosest session in the chain, not the tightest, because the sessions do not coordinate. Bounding the IdP session limits how long single sign-on stays convenient. It does nothing to the app session that is actually answering requests.&lt;/p>
&lt;h2 id="single-logout-was-supposed-to-fix-this">Single Logout Was Supposed to Fix This
&lt;/h2>&lt;p>The standards bodies saw this coming and built single logout, the mechanism for ending all those sessions at once. It has a long and unhappy history.&lt;/p>
&lt;p>SAML 2.0 has a Single Logout profile, and in practice it is fragile. The front-channel version walks the user&amp;rsquo;s browser through a chain of logout redirects, and if any one participant is slow or unreachable, the chain breaks and some sessions stay open. Most deployments either never enable it or never trust it.&lt;/p>
&lt;p>OIDC split the problem in two, and both halves are now finished specifications rather than the drafts they spent years as. &lt;a class="link" href="https://openid.net/specs/openid-connect-frontchannel-1_0.html" target="_blank" rel="noopener"
>Front-Channel Logout&lt;/a> loads each app&amp;rsquo;s logout URL in a hidden iframe; it was never robust, and the browser industry&amp;rsquo;s move to block third-party cookies has largely finished it off. &lt;a class="link" href="https://openid.net/specs/openid-connect-backchannel-1_0-final.html" target="_blank" rel="noopener"
>Back-Channel Logout&lt;/a> is the reliable one: the IdP sends a logout token straight to each app&amp;rsquo;s server, no browser involved. It works, when the app implements a back-channel logout endpoint, and a great many apps simply do not. A standard reaching final status does not make the long tail of vendors support it.&lt;/p>
&lt;h2 id="revoke-the-session-with-a-signal">Revoke the Session With a Signal
&lt;/h2>&lt;p>The approach that actually fits the problem is newer, and it gives up on propagating a logout in favor of broadcasting a fact. The OpenID Foundation&amp;rsquo;s &lt;a class="link" href="https://openid.net/specs/openid-sharedsignals-framework-1_0-final.html" target="_blank" rel="noopener"
>Shared Signals Framework&lt;/a> and its &lt;a class="link" href="https://openid.net/specs/openid-caep-1_0-final.html" target="_blank" rel="noopener"
>Continuous Access Evaluation Profile&lt;/a> reached final status together, and CAEP defines a &lt;code>session-revoked&lt;/code> event for exactly this. The IdP, acting as a transmitter, pushes a signed event to each participating app, the receiver, that says this subject&amp;rsquo;s session is revoked. The app drops the session in near-real-time, without waiting for a token to expire or a redirect to fire.&lt;/p>
&lt;p>This is the model that closes the gap short token lifetimes cannot. Microsoft ships it as &lt;a class="link" href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-continuous-access-evaluation" target="_blank" rel="noopener"
>Continuous Access Evaluation&lt;/a>, where a termination or a risky sign-in can end active sessions in about a minute rather than waiting out the token. Okta and Google are building Shared Signals support. The direction is clear: instead of chaining a logout through every app, broadcast the revocation as a fact and let each app act on it.&lt;/p>
&lt;p>&lt;img src="https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/diagram-logout-vs-signal.png"
width="1476"
height="1264"
srcset="https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/diagram-logout-vs-signal_hu1081248231368365481.png 480w, https://mattgoodrich.com/posts/logging-out-is-harder-than-logging-in/diagram-logout-vs-signal_hu9982273169649613285.png 1024w"
loading="lazy"
alt="Three Ways to End Sessions: Front-Channel Logout Loads Each Apps Logout URL in a Browser Iframe and Is Broken by Third-Party Cookie Blocking; Back-Channel Logout Sends a Server-to-Server Logout Token but Only Reaches Apps That Implement It; Shared Signals and CAEP Have the IdP Push a session-revoked Event to Receiving Apps That Drop the Session in Near-Real-Time"
class="gallery-image"
data-flex-grow="116"
data-flex-basis="280px"
>&lt;/p>
&lt;h2 id="what-you-can-actually-ship-today">What You Can Actually Ship Today
&lt;/h2>&lt;p>Shared Signals is the right model, and it is not a switch you flip, because it only works where both ends implement it. Your IdP has to transmit and every app has to receive, and the app long tail that never adopted back-channel logout is the same long tail that will not have a Shared Signals receiver for years. For those apps you are back to waiting out the session.&lt;/p>
&lt;p>So the deployable answer is layered, weakest to strongest. Set short absolute lifetimes on the sessions and tokens you control, accepting the extra re-authentication, because a short fixed session is the one mitigation that works without the app&amp;rsquo;s cooperation. Revoke the refresh token at the IdP through &lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc7009" target="_blank" rel="noopener"
>OAuth token revocation&lt;/a> the moment the account is disabled, so it stops minting new access tokens even if nothing else fires. Turn on &lt;a class="link" href="https://openid.net/specs/openid-connect-backchannel-1_0-final.html" target="_blank" rel="noopener"
>back-channel logout&lt;/a> for the apps that support it; the open-source identity providers implement it, &lt;a class="link" href="https://www.keycloak.org/" target="_blank" rel="noopener"
>Keycloak&lt;/a>, &lt;a class="link" href="https://goauthentik.io/" target="_blank" rel="noopener"
>Authentik&lt;/a>, and &lt;a class="link" href="https://www.ory.sh/hydra/" target="_blank" rel="noopener"
>Ory Hydra&lt;/a>, if you want to run the mechanism end to end. Adopt Shared Signals and CAEP between your IdP and the apps where a terminated session has to die in minutes, not hours; SGNL&amp;rsquo;s &lt;a class="link" href="https://caep.dev/" target="_blank" rel="noopener"
>caep.dev&lt;/a> is a free CAEP transmitter to test against, with an &lt;a class="link" href="https://github.com/SGNL-ai/caep.dev" target="_blank" rel="noopener"
>open-source receiver library&lt;/a> to start from. And for everything still on a sliding session you cannot signal, the honest move is to know it is there and shorten it where you can, instead of assuming logout did something it did not.&lt;/p>
&lt;h2 id="logging-in-is-a-handshake-logging-out-is-a-distributed-systems-problem">Logging In Is a Handshake; Logging Out Is a Distributed-Systems Problem
&lt;/h2>&lt;p>Logging in is a handshake: one exchange, one answer, and you are in. Logging out is several independent sessions, set by different teams with different timeouts, none of them watching the others, and no single place that can end them all at once. The standards that promised to close them in one click never quite delivered, and the model that works, pushing a revocation signal to every app, only helps where the app is listening.&lt;/p>
&lt;p>The practical version is unglamorous. Know how many sessions a login actually creates, keep the ones you control short, signal the ones you can, and stop treating a disabled account as a closed session. The account is the door. The session is the person already inside.&lt;/p></description></item></channel></rss>