Repository navigation
feat(auth): ask to sign in again when the session expires - #321
Conversation
When the sign-in session ended while a page was open, the page kept showing the old view and every action failed with its own confusing error, because API routes answered 401 AUTH_SESSION_MISSING. With single sign-on the session has a fixed lifetime (OIDC_SESSION_MAX_AGE, 24 h by default, no refresh), so this happened every day to anyone who kept a tab open. - A SessionWatcher in the signed-in app layout schedules a session check at the session's expiresAt and re-checks when the tab becomes visible or the browser comes back online (at most once a minute). Each check re-reads expiresAt; a check that returns an already-past expiresAt (clock skew) is retried after 30, 60 and 120 s. A network error or 5xx is never treated as expiry. There is no periodic polling. - One shared window fetch observer inspects a clone of same-origin /api/ responses with status 401 (not /api/auth/*) asynchronously, bounded to 16 KiB and one second, and raises the prompt on code AUTH_SESSION_MISSING. Responses are returned to callers unchanged and undelayed; REAUTHENTICATION_REQUIRED, 403 and other codes are ignored. - The prompt is a modal: "Your sign-in has expired" with "Sign in again" (a full page load to /login?returnTo=<current page>) and "Later". It closes again if a later check finds the session restored. - toastApiError stays silent for a 401 AUTH_SESSION_MISSING response, so the modal replaces the per-action error toasts; every other error still shows. - The login page accepts returnTo, validated as a same-origin relative path (no //, backslash, control characters or scheme), and honours it after password sign-in, for an already signed-in visitor, and through SSO, where the callback is an absolute URL on the app origin because better-auth rejects relative callbacks with a fragment or colon. When the provider cancels before better-auth reads the OAuth state, the destination is recovered from sessionStorage for the retry. Session lifetime, disableSessionRefresh and the /api/v1 contract are unchanged.
Review: feat(auth): ask to sign in again when the session expiresSolid, well-tested feature: a proactive Verdict: REQUEST_CHANGES Findings: Blocker 0 | Critical 1 | Major 0 | Minor 0 | Nit 0 FindingsCRITICAL - Missing testsNone beyond the above - coverage for the watcher timing, throttling, skew retry, 401 detection/exclusions, and return-path validation is thorough. An integration-style test that renders the actual |
The login page always passed a validated returnTo (falling back to /dashboard) to SsoLogin, so the client's sessionStorage fallback never ran: after a cancellation better-auth redirects to /login?error=... without returnTo and the retry landed on the dashboard. The page now forwards returnTo only when the query has one; the redirect of a signed-in visitor still uses the validated destination.
Review: feat(auth): ask to sign in again when the session expiresSecond pass, after the fix in 7f7b56a. Walked every changed file against The Verdict: APPROVE Findings: Blocker 0 | Critical 0 | Major 0 | Minor 1 | Nit 0 FindingsMINOR (inline, Missing testsNone beyond the above - the suite already exercises the scenario that mattered most (provider cancellation + retry against a real better-auth instance), plus throttling, skew retry, long-timer capping, 401 detection/exclusions/cleanup ordering, and tampered/unavailable sessionStorage. |
The fetch observer snapshotted its listeners before awaiting the request and notified only that snapshot, so a watcher mounted during the request missed an AUTH_SESSION_MISSING response. It now notifies the listeners registered when the response is inspected.
|
Review: feat(auth): ask to sign in again when the session expires Third pass, after fix commit The fix replaces the stale No regressions: the round-1 SSO-cancellation fix ( Verdict: APPROVE Findings: Blocker 0 | Critical 0 | Major 0 | Minor 0 | Nit 0 Findings: None. The minor concern from the previous round is fixed and covered by a new test. Missing tests: None beyond what is already there. |
Summary
When the sign-in session ended while a page was open, the page kept showing the old view and every action failed with its own confusing error. API routes answer
401 AUTH_SESSION_MISSINGin that case. With single sign-on the session has a fixed lifetime (OIDC_SESSION_MAX_AGE, 24 h by default, no refresh), so anyone who keeps a tab open hits this daily.Changes
SessionWatcherin the signed-in app layout:expiresAt;expiresAton every check, and retries a check that returns a pastexpiresAt(clock skew) after 30, 60 and 120 s;fetchobserver inspects a clone of same-origin/api/401 responses (not/api/auth/*), asynchronously and bounded to 16 KiB and one second. On codeAUTH_SESSION_MISSINGit raises the prompt.REAUTHENTICATION_REQUIRED, 403 and other codes are ignored./login?returnTo=<current page>) and "Later". It closes again when a later check finds the session restored.toastApiErrorstays silent for a401 AUTH_SESSION_MISSINGresponse, so the modal replaces the per-action error toasts. Every other error still shows./loginacceptsreturnTo, validated as a same-origin relative path: no//, backslash, control characters or scheme. It is honoured:sessionStoragefor the retry.Session lifetime,
disableSessionRefreshand the/api/v1contract are unchanged.Testing
src/tests/auth/: watcher timing, throttling, skew retry, restored sessions; 401 detection and exclusions, undelayed responses, observer cleanup order; return-path validation against encoded redirect attacks; SSO callbacks through better-auth's real validator, including cancellation.devwithout this branch. They are pre-existing, not caused by this change, and will be reported separately./login?returnTo=…and returns to the same agenda after sign-in.