Session Timeout Policy Across a Product Suite

The security review had been going well for about forty minutes.

Then their lead engineer put up a slide with one line on it: All sessions must expire after 15 minutes of inactivity. Not the admin console. Not the reporting tool. All of them — six applications, including the tablet app their field technicians carry around a substation for eight hours and the shared terminal in the dispatch office.

The vendor's architect did what everyone does. He explained that the applications had different risk profiles, that the admin console already had a 15-minute timeout, that the field app would be unusable, and that surely the intent of the control was — at which point the compliance lead said the sentence that ends these conversations: "Fifteen minutes is in our standard. Can you do it or not?"

He could do it. He did do it. Within two months the field technicians had discovered that leaving a video playing in a second tab kept the session alive all day, and the dispatch terminal had a laminated card next to it with the shared login on it, because typing the password nineteen times a shift is not a thing humans do.

The customer got their 15 minutes. Nobody got any security.

I've watched some version of this happen enough times to believe the failure isn't in the negotiation. It's that almost nobody walks into that room with a method — a way of deriving each number from something other than vibes, and a way of describing the result that a compliance team can accept without you flattening the whole suite. The mechanics of how these sessions relate to each other are covered elsewhere: the four kinds of state, what they're actually made of, and what happens when they fall out of sync. This article is about the part those leave open. Given all that machinery, what numbers do you actually pick, and how do you defend them?

Four clocks, and the one rule that keeps them coherent

Every "am I still logged in" question in a suite is answered by four independent timers:

  • the IdP SSO session — how long the identity provider will answer /authorize without prompting;
  • the application session — how long each RP considers you logged into it;
  • the access token TTL — how long a resource server will accept a credential without re-checking;
  • the refresh token lifetime — how long a client can mint new access tokens without user interaction.

They are configured in four different places, usually by four different people, frequently in four different quarters. The suite is coherent only if one rule holds:

An application's effective session cannot outlive the SSO session that authorized it, and a re-authentication at the IdP must be able to shorten a downstream session, not merely extend it.

The first half is violated constantly, and the violation has a specific shape: the refresh token lives longer than the SSO session. The IdP session is 12 hours; the refresh token is 30 days with sliding renewal. At hour thirteen the IdP no longer knows the user — but the SPA doesn't call /authorize, it calls /token, and the token endpoint happily honours a refresh token that has no dependency on the SSO session that produced it. The user is now logged into an application on the authority of an authentication event the IdP has forgotten.

That's the real source of "I logged out of one app and I'm still in the other." The user hits the end-session endpoint from App A, the SSO session dies, and App B's refresh token keeps working because nothing linked the two lifetimes. Not a bug in either app. Nobody ever wrote down that a refresh token is a derivative of an authentication event and should not survive it.

flowchart TD
    SSO["IdP SSO session<br/>bounds everything below it"]
    SSO --> AS["Application session<br/>≤ SSO session"]
    SSO --> RT["Refresh token<br/>≤ SSO session, and bound to sid"]
    RT --> AT["Access token<br/>short, expiry is the only control"]
    AS -.->|"re-auth must be able<br/>to SHORTEN this"| SSO

The second half of the rule — that re-authentication can shorten — is subtler and it's where most suites are quietly broken. When a user re-authenticates at the IdP, auth_time resets and everyone treats that as extending the world. But the interesting direction is downward: an application with a stricter policy than the IdP needs a way to enforce it, and an IdP-side event (a step-down, an admin forcing re-auth, a device becoming untrusted) needs a way to reach applications whose sessions are longer. The first direction is max_age, which every RP can use and few do. The second direction is back-channel logout, which is the only broadly-implemented mechanism the IdP has for shortening something downstream — and it is all-or-nothing. There is no "reduce App B's session to 15 minutes" message in OIDC. You get "end it," or you get nothing.

Design accordingly: put the strictness in the RP that needs it, because the IdP can only terminate, never tighten.

The central mistake: deriving both timeouts from sensitivity

Here is the thing I'd most like people to take from this article, because once you see it you cannot configure a session store the old way again.

Idle timeout and absolute timeout answer different threat models, and should be derived from different inputs. Most teams derive both from "how sensitive is the data in this app," which is why their numbers are indefensible — not wrong, exactly, but unable to survive the question "why 30 and not 20?"

Idle timeout is about the unattended device. The threat is a human being who is not the user, standing in front of a screen the user walked away from. That threat has essentially nothing to do with what the application contains and everything to do with where the session physically lives. A 30-minute idle timeout on a personal laptop in a locked home office and a 30-minute idle timeout on a shared kiosk in a hospital corridor are not the same control, because the probability of an unauthorized human reaching the screen differs by orders of magnitude. So derive it from the device context: shared terminal, kiosk, hot-desked workstation, managed personal laptop, mobile device with its own OS-level lock.

This has a liberating consequence. If the operating system already enforces a 5-minute screen lock with re-auth, and you can verify the device is managed, the application's idle timeout is a second control on the same threat, and it can be much longer without weakening anything. The OS lock is a better control than yours — it covers all applications and it can't be defeated by a video playing in another tab.

Absolute timeout is about credential staleness and revocation latency. The threat is that something changed about the user — they were terminated, moved teams, had their access revoked, had their laptop stolen — and the session doesn't know. The absolute lifetime is the outer bound on how long a stale authorization can keep working. And that means it should be derived from an operational number, usually a contractual one: how quickly must a deprovisioned employee actually lose access? Twenty-four hours is common. Four hours appears in some regulated environments. "Immediately" appears in contracts written by people who have not thought about it.

Which sets up the second-order point, the one worth arguing with your compliance team about:

Absolute session lifetime is a compensating control for the revocation you don't have.

If a termination in the HR system propagates to the IdP within minutes, and the IdP fires back-channel logout to every RP, and your resource servers check something live on sensitive paths, then your revocation latency is minutes and it does not depend on session expiry at all. The absolute lifetime is a backstop for the cases where that chain fails. If your revocation chain is a nightly batch job, the absolute lifetime is your revocation, and it had better be shorter than your contractual SLA.

That's a real, arguable position with a real consequence: investing in revocation buys you longer sessions. It's the only argument I know that lets you ask for a 12-hour absolute lifetime and be taken seriously, because you're not asking for less security — you're pointing out that you've moved the control somewhere it works better.

A worked suite

Concrete numbers, because "it depends" is not a method. Here's a suite I'd defend in a review, assuming an IdP SSO session of 12 hours absolute / 4 hours idle, device management on corporate endpoints, and back-channel logout working end to end.

Application Idle Absolute Why these numbers
Admin console (tenant config, user management) 30 min 8 h Idle from device: managed laptops in offices, OS lock at 10 min already covers the unattended case. Absolute from role: admins are the population where a stale session is most consequential, and 8h means a session cannot cross from one working day into the next. Destructive operations are step-up gated regardless of session age.
Reporting / analytics 30 min 12 h Same device class as the admin console, so the same idle number. Read-only aggregates with no write path, so the absolute lifetime is set purely by the revocation backstop (24h contractual SLA, halved for margin). Sensitivity of the data is not an input here — it's an input to whether exports require step-up, which they do.
Customer-facing dashboard 60 min 12 h Unmanaged personal devices, but a single user's own data on their own machine — the unattended-device threat is real but the blast radius is one account. A 15-minute timeout here produces support tickets and no measurable risk reduction. Re-auth required to change email, MFA, or payment details.
Mobile field app none (app lock instead) 30 d refresh, 7 d without network Idle timeout is the wrong control on a device with biometric unlock and an OS keychain. Use an in-app lock on foreground (Face ID / device passcode) with an immediate trigger, and let the session persist. Absolute is a refresh-token lifetime bound to device attestation and revocable server-side; a device out of network contact for 7 days stops working offline.
Kiosk / shared dispatch terminal 2 min end of shift (8 h max) The one place a very short idle timeout is correct, because the unattended-device probability is near 1. Pair it with badge-tap or PIN resume rather than full re-auth — the control is "the previous user's screen is gone," which a lock screen satisfies. Absolute bounded by the shift so a session never carries across a handover.
Partner / API portal 30 min 8 h External identities, federated from customer IdPs you don't control. Shorter absolute than internal apps not because the data is more sensitive but because your visibility into the partner's deprovisioning is zero — the absolute lifetime is doing all the revocation work here, since you will not get a back-channel logout when their employee leaves.

Note what's doing the work in that table. The idle column tracks device context almost perfectly and data sensitivity not at all. The absolute column tracks revocation quality and blast radius. Sensitivity shows up in the third place — the step-up boundary — which is exactly where it belongs, because the sensitive thing is an operation, not a session.

Step-up is the release valve that makes all of this affordable. If deleting a tenant, exporting a customer list, or changing MFA settings requires fresh, strong authentication bound to that operation, then a long session is not a long window of dangerous capability. It's a long window of reading things. The full argument for that — including why the resource server, not the client, should be issuing the challenge — is in Step-Up Authentication: The Right Way to Handle Sensitive Operations.

Why flattening to the strictest number is worse than it looks

The default outcome of the meeting at the top of this article is that every application in the suite gets the strictest number anyone asked for. It's the path of least resistance: nobody was ever fired for being too strict, and per-app policy requires per-app justification.

It is also actively harmful, in three specific ways.

It doesn't improve security, because the sensitive application was already protected. The admin console had 15 minutes before the meeting. Applying 15 minutes to the reporting tool didn't reduce risk in the admin console, and the reporting tool's actual risk — an analyst exporting a customer list — is not a function of session duration at all. You have added friction in exactly the places where the threat model didn't call for it, which is the definition of security theatre even when every individual control is real.

It trains users to defeat it. This is the part that gets waved away and shouldn't. When a timeout is shorter than the natural rhythm of the work, people don't re-authenticate more; they build machinery to avoid re-authenticating. Mouse jigglers, a second tab with a polling dashboard, a video, a script that clicks every four minutes, the laminated card next to the shared terminal. Every one of these is worse than the timeout you were trying to enforce, and — this is the sharp bit — they defeat the idle timeout specifically while leaving the absolute timeout intact, which means the control you accidentally kept is the one the compliance team wasn't asking about.

There's a corollary worth stating: an idle timeout is only enforceable if "idle" is measured from real human interaction. If your definition of activity includes background token refreshes or XHR polling, you don't have an idle timeout, you have a rounding error. That's the failure mode described in the session sync article, and it's why a strict number and a loose activity signal together are the worst combination available: maximum user pain, zero enforcement.

It makes the number unenforceable in the place it mattered. Once every app has a 15-minute timeout, the volume of re-authentication is high enough that users optimize it away globally — including in the admin console. You didn't raise the floor. You lowered the ceiling.

The correct move is almost always the opposite trade: a longer session with a hard boundary in front of the dangerous operation beats a short session with no boundary. One asks for strong proof at the moment of consequence, rarely enough that the prompt still means something. The other asks for weak proof constantly, until the prompt means nothing and the user has automated it.

The compliance conversation, practically

Here's what's usually happening when a customer demands 15 minutes.

The number is almost always inherited. PCI DSS requires re-authentication after 15 minutes of idle time — for sessions with access to the cardholder data environment, which in practice means administrative interfaces, not every web application a company runs. HIPAA has no number at all; the Security Rule's automatic logoff provision is an addressable specification with no duration attached, and the 15-minute figure attached to it in a thousand internal standards is folklore that hardened into policy. NIST SP 800-63B, which is the most useful document to have open in these meetings, specifies 30 minutes of inactivity and a 12-hour absolute reauthentication limit at AAL2, and 15 minutes and 12 hours at AAL3. Notice that even the strict tier pairs a short idle window with a long absolute one — the framework itself treats them as different controls with different inputs.

So the customer's standard usually says 15 minutes because someone copied a workstation-lock control into a document that now applies to web sessions, and nobody has had a reason to reopen it.

The way to reopen it is not to argue about the number. It's to move the conversation up one level, to the control, and then map your architecture onto it. Something close to this, which I've seen work:

"The control here is unattended sessions must not remain usable by another person. We satisfy that control across the suite, but not with the same mechanism everywhere, because the exposure differs.

On the shared dispatch terminal, we satisfy it with a 2-minute idle timeout and badge-resume, because that's the one place where an unattended screen is likely to be reached by someone else.

On managed workstations, we satisfy it with a 30-minute application idle timeout layered on your existing 10-minute OS lock, which is a stronger control than ours because it covers every application at once and can't be defeated by a background browser tab.

On mobile, we satisfy it with device binding plus biometric app-unlock on every foreground, which enforces presence more strictly than any timeout would.

And separately from all of that, we bound credential staleness with an 8- to 12-hour absolute lifetime, and we re-authenticate at these specific operations regardless of session age: [list them].

If it would help, we can provide this as a control mapping against your standard, showing which mechanism satisfies each clause per application."

That last offer is the one that actually closes the conversation, because it gives the compliance lead something to file. Their problem is rarely that they believe in 15 minutes. Their problem is that they need a defensible artifact, and "the vendor said it was fine" is not one. A per-application control mapping is. Write it once; you will use it in every deal.

Two honest caveats.

Sometimes the customer is right and you should just do it. If the application is an administrative console in a card data environment, the control says 15 minutes and it means 15 minutes, and arguing is a waste of everyone's afternoon. Know which of your applications are actually in scope for the framework being cited, and concede those instantly and cheerfully. It buys enormous credibility for the applications where you're going to push back.

Don't offer flexibility you can't operate. A control mapping with six different mechanisms is a promise that all six work. If your back-channel logout is half-implemented or your device binding is a header the client sets, you've just committed to being audited on something you don't have. Fewer mechanisms, actually enforced, is a much better position than a beautiful matrix.

The mechanics that make per-app policy real

Per-application timeouts only work if the enforcement points exist. Three that get skipped:

The RP must send max_age and verify auth_time. This is the single most common gap. An application with a 30-minute policy that redirects to /authorize with no max_age will get a silent, instant assertion from a 12-hour SSO session, and its 30-minute policy becomes decorative — the auditor refreshes the page and is straight back in. Sending max_age=1800 tells the IdP not to satisfy the request silently unless the user authenticated within that window. Then verify the returned auth_time, because max_age handling is inconsistent across implementations and an IdP that ignores it will return a perfectly valid token that doesn't meet your requirement. Trusting it is how a policy passes testing and enforces nothing.

Know the difference between prompt=login and a silent re-auth. prompt=login forces a fresh authentication interaction unconditionally. max_age forces it only if the existing authentication is too old. Use max_age for policy — it's idempotent and it doesn't punish a user who authenticated ninety seconds ago in another app. Reserve prompt=login for deliberate re-authentication events, and be aware it says nothing about which factor was used; if you need assurance rather than freshness, that's an acr requirement, not a prompt.

Back-channel logout must be wired before you differentiate lifetimes. The moment applications have different session lengths, the case "IdP session ended while App B still has four hours left" becomes a routine occurrence rather than an edge case. Without back-channel logout, App B has no idea and keeps working — with a stale authorization, exactly the thing your absolute lifetime was supposed to bound. This requires storing the sid claim alongside each application session, which is the step teams skip. Differentiated lifetimes without back-channel logout isn't per-app policy; it's per-app drift.

The mobile app everyone wants logged in for 90 days

This is the request that breaks the framework, so let's take it head on.

Product wants the field app to stay logged in essentially forever, because a technician on a rooftop with one bar of signal cannot do a federated login. Security wants the same 8-hour absolute lifetime as everything else. Both are right.

The resolution is that a 90-day mobile session is acceptable if and only if it isn't really a session — if it's a device-bound credential you can revoke, plus a presence check you can't skip. Concretely: bind the refresh token to the device with something the client can't forge (attestation, a hardware-backed key, mTLS, DPoP); require biometric or passcode unlock on every foreground, so possession of the device isn't sufficient; require network contact within some bounded window so an offline stolen device stops working; keep a short access token TTL so revocation propagates in minutes rather than days; and make the sensitive operations step-up gated with a fresh check, so a long-lived session doesn't equal long-lived capability.

Now the honest part, which you should say out loud in the review rather than hoping nobody asks: the 90-day mobile session is a real risk transfer, and the thing absorbing it is your revocation path. If your ability to kill that device's refresh token is a manual ticket to an ops team, you have a 90-day breach window and no amount of attestation fixes it. If it's a button in the admin console that takes effect within an access-token TTL, you have a defensible design.

Which is the same rule as everywhere else in this article, in its most extreme form. You are always trading session length against revocation quality. The teams that end up with defensible numbers are the ones who made that trade explicitly, per application, and wrote down what each number is compensating for.

The teams that end up with 15 minutes everywhere made the same trade. They just made it once, for the whole suite, in a meeting, under pressure, and then paid for it in mouse jigglers.