Concepts
Session lifecycle: idle, absolute, and refresh windows
How Authio sessions stay alive, when they roll over, and the three knobs every customer can configure per organization.
Part of Authio Lobby
Every Authio sign-in mints a pair of tokens. Understanding which token controls what is the first step to picking sensible session settings for your app — and to debugging the rare cases where a user is bounced back to the sign-in screen at an unexpected moment.
The two tokens
- Access token — a short-lived JWT (~15 minutes) you attach to every API call. Edge runtimes verify it locally against the JWKS endpoint with no round-trip. Stored server-side in your BFF’s short-lived session cookie (the dashboard uses
authio_dashboard_session). - Refresh token — an opaque, long-lived string (default 30 days) you exchange for a new access token when the access token nears expiry. Stored server-side in your BFF’s long-lived refresh cookie (the dashboard uses
authio_dashboard_refresh) and never sent to the browser. Rotated on every successful refresh; the previous token becomes invalid the moment a new one is issued.
The pattern is the standard backend-for-frontend (BFF) cookie auto-renewal flow: access cookies expire often, refresh cookies live long, and the BFF silently rotates them in place. End users never see the sign-in screen until the refresh chain itself ends.
The three policy knobs
Customers configure their session lifecycle on the dashboard at /orgs/<id>/security. Each knob is a per-org override; a value of 0 means “inherit the project default”. The strictest non-zero gate wins on every refresh.
| Knob | Anchored to | What it controls |
|---|---|---|
| session_idle_timeout_min | last_active_at | Force re-auth when the gap from the user’s last API call to now exceeds this. Roll-forward — every refresh resets the clock. |
| session_absolute_max_min | issued_at | Hard cap on session lifetime since the original sign-in, regardless of activity. Forces a clean credential check at this interval. |
| refresh_window_min | issued_at | Cap on the rolling refresh-token chain. Refresh tokens rotate on every silent renewal, but the chain cannot extend past this window. Typically shorter than session_absolute_max_min. |
When a session trips one of these gates, auth-core returns an HTTP 401 with a structured code so your BFF can render the right copy:
policy_violation_session_idle // last_active gap > idle limit
policy_violation_session_absolute // issued_at + absolute < now
policy_violation_session_refresh_window // issued_at + refresh_window < nowSensible defaults by threat model
Consumer SaaS (low-friction)
session_idle_timeout_min: 0 (inherit project default — typically generous, e.g. 8h)session_absolute_max_min: 43200 (30 days)refresh_window_min: 43200 (30 days)
Optimise for “don’t make me sign in again”. Combine with passive risk signals (risk engine + impossible-travel detection) so you only step up auth when something looks off.
Enterprise B2B (compliance-aware)
session_idle_timeout_min: 240–480 (4–8h)session_absolute_max_min: 10080 (7 days)refresh_window_min: 1440–10080 (1–7 days)
Most SOC 2 / SOX shops want users to re-auth at least weekly. Pair with require_sso for sensitive orgs so the re-auth round-trip goes through the customer’s IdP.
Healthcare / HIPAA (tight)
session_idle_timeout_min: 15–30session_absolute_max_min: 1440 (24h)refresh_window_min: 720 (12h)
HIPAA §164.312 doesn’t mandate a specific timeout but the standard interpretation is “short enough that an unattended workstation can’t leak PHI”. 15-minute idle is the canonical EHR setting; the 24h absolute cap forces a daily clean re-auth for audit-trail purposes.
Refresh from a Node BFF
import { Authio, AuthioError } from "@authio/node";
const authio = new Authio({ apiKey: process.env.AUTHIO_SECRET_KEY! });
// Inside your /api/auth/refresh route handler:
try {
const env = await authio.sessions.refresh({
refreshToken: req.cookies["authio_refresh"],
});
res.cookie("authio_session", env.access_token, {
httpOnly: true, secure: true, sameSite: "none",
maxAge: 15 * 60 * 1000,
});
res.cookie("authio_refresh", env.refresh_token, {
httpOnly: true, secure: true, sameSite: "none",
maxAge: 30 * 24 * 60 * 60 * 1000,
});
res.redirect(req.query.next ?? "/");
} catch (err) {
if (err instanceof AuthioError && err.code.startsWith("policy_violation_")) {
// The session aged out per the org-policy gate.
res.clearCookie("authio_session");
res.clearCookie("authio_refresh");
res.redirect(`/sign-in?err=${err.code}`);
return;
}
throw err;
}Auto-refresh from React
@authio/react’s AuthioProvider ships with a built-in background refresher. It reads a non-HttpOnly hint cookie (authio_session_exp) the BFF sets at sign-in to schedule the next refresh exactly refreshLeadSeconds before the access JWT expires.
import { AuthioProvider, useRefreshSession } from "@authio/react";
<AuthioProvider
publishableKey={import.meta.env.VITE_AUTHIO_PUBLISHABLE_KEY}
refreshUrl="/api/auth/refresh"
refreshLeadSeconds={60}
>
{children}
</AuthioProvider>;
// Inside any component, force a refresh manually (e.g. on tab focus):
function StayFreshOnFocus() {
const refresh = useRefreshSession();
React.useEffect(() => {
const onFocus = () => void refresh();
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
}, [refresh]);
return null;
}Where the gates fire
The policy resolver consults org_policies before these ceremonies:
- Session refresh —
POST /v1/sessions/refresh(and its aliasPOST /v1/auth/refresh): all three knobs are checked. The rotated refresh token’s expiry is also clamped toissued_at + refresh_window_min. - Magic-link send / OAuth start / passkey register: the auth-method gates run (require_sso / require_passkey / require_mfa). Lifecycle knobs do not apply to the initial sign-in by definition.
- Network policies on every refresh: the IP allow-list and country block-list are also re-evaluated. Sessions that move outside the allow-list are revoked at the next refresh.
/orgs/<your-org>/security, set session_idle_timeout_min to 1, wait 90 seconds, and try to navigate. You’ll see the BFF bounce to /sign-in?err=policy_violation_session_idle. Set it back to 0 when you’re done — the platform’s own dashboard inherits the project default.