* feat(auth): enforce session idle and absolute timeouts
Hosted browser sessions now carry an HMAC-signed, HttpOnly cookie holding
session start, last activity and sign-in method, bound to the Supabase
session. Middleware enforces a 30 min idle and 12 h absolute limit
(reason-coded redirects to /login), a heartbeat route advances idle
activity from real user input, and a client controller warns 2 minutes
before expiry. BankID users are routed back to BankID on re-auth via a
short-lived method hint. API-key and MCP bearer surfaces are exempt;
self-hosted installs default off and can opt in via env vars.
Fixes#362
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): derive session-timeout signing key via HKDF
The HMAC key is now HKDF-derived with a purpose-bound info string, so
the SUPABASE_SERVICE_ROLE_KEY fallback never uses the privileged
credential directly as a signing key. Addresses the security review
finding on PR #1387.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): back signature bytes with a plain ArrayBuffer
crypto.subtle.verify requires a BufferSource; Uint8Array.from is typed
over ArrayBufferLike, which the Vercel TypeScript build rejects. Decode
base64url into a Uint8Array constructed over a fresh ArrayBuffer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): address session-timeout review findings
- signSessionTimeoutState returns null on signing failure instead of
throwing, so a missing secret degrades the timeout feature in line
with verifySessionTimeoutState rather than crashing authenticated
requests; middleware and heartbeat skip the cookie write when null
- heartbeat initializes a fresh signed state for a missing or
session-mismatched cookie, mirroring middleware, instead of
returning SESSION_EXPIRED during normal initialization
- sessionStateMatchesUser treats an unresolved current session id as
a mismatch for session-bound state so another session's cookie is
never accepted on the userId fallback alone
- drop aria-live from the countdown DialogDescription so screen
readers are not interrupted every second
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
connect-src listed the https Supabase origin plus wss://*.supabase.co,
but never the wss variant of a self-hosted Supabase URL. Supabase
Realtime opens wss://<host>/realtime/v1/websocket, which CSP blocked;
WebKit throws synchronously on a CSP-blocked new WebSocket(), so Safari
unmounted the dashboard into the error boundary (Chromium only logs).
- next.config.ts: add supabaseWsUrl (NEXT_PUBLIC_SUPABASE_WS_URL, or
the Supabase URL with https to wss / http to ws) to connect-src
- Dockerfile: bake a __NEXT_PUBLIC_SUPABASE_WS_URL__ sentinel, since
the CSP is fixed at build time and only sed-substituted at runtime
- docker-entrypoint.sh: derive the wss origin from
NEXT_PUBLIC_SUPABASE_URL unless overridden, substitute the sentinel
- .env.docker.example: document the optional override
Hosted is unaffected: the wss form of *.supabase.co was already
allowlisted, so the added token is redundant there.
Fixes#893
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add .env.docker.example as a template for Docker-based local
development, and add a negation pattern to .gitignore so the
example file is tracked while real .env* files stay ignored.
Signed-off-by: Fabian Mossberg <fabian@hejfaktura.se>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>