fix(auth): silently refresh standalone (non-OIDC) sessions (MAPPS-374) #435

Merged
nrupard merged 1 commit from fix/MAPPS-374-standalone-silent-refresh into main 2026-07-16 17:09:01 +02:00
Owner

Summary

Fixes MAPPS-374: standalone (legacy email+password) sessions had no silent refresh, so users were logged out roughly one access-token lifetime (~1h) after signing in.

Root cause

The SPA has two auth paths. The OIDC path (bunyip-as-OP) has use_token_refresh (src/hooks/auth.rs), which rotates the ~10-min at+jwt before expiry. The standalone path had no refresh loop - the deferred follow-up called out in src/pages/login.rs. A standalone session stores { access_token, refresh_token, expires_at, user } in sessionStorage (STANDALONE_KEY) with AuthContext.tokens = None, so the OIDC loop skips it. When mokosh-server's 1-hour standalone access token expired, the next 401 hit refresh_user_from_me, which cleared the session - even though a valid 7-day (30-day remember-me) refresh token was sitting unused in sessionStorage.

Fix (client-only)

Add use_standalone_token_refresh, the standalone twin of use_token_refresh, mounted once at the app root (main.rs). Each 30s tick reloads the persisted StandaloneSession and, once the access token is within 60s of expiry, rotates it via POST /api/v1/auth/refresh (endpoint already exists, returns rotated tokens), persisting the result to sessionStorage and the fetch access-token holder. On any refresh failure it clears the session and redirects to /login, matching the OIDC error branch.

  • No-ops when no standalone session is stored; the OIDC loop still no-ops for standalone (tokens = None) - each hook owns exactly one session type.
  • Tokens stay in per-tab sessionStorage and the loop is a single root instance, so there is no refresh-token-reuse race (unlike the OIDC family-reuse footgun).
  • No server change: POST /api/v1/auth/refresh already rotates and returns { access_token, refresh_token, expires_at }.

Testing

cargo fmt, cargo clippy --all-targets -- -D warnings, and cargo check --target wasm32-unknown-unknown all green in the pinned rust-builder image.

Scope

This addresses only the standalone logout. The separate bunyip/OIDC logout (~10 min, a failing OIDC refresh) is being pinned from live logs/console evidence and will get its own issue + fix.

## Summary Fixes MAPPS-374: standalone (legacy email+password) sessions had no silent refresh, so users were logged out roughly one access-token lifetime (~1h) after signing in. ## Root cause The SPA has two auth paths. The OIDC path (bunyip-as-OP) has `use_token_refresh` (`src/hooks/auth.rs`), which rotates the ~10-min at+jwt before expiry. The standalone path had **no** refresh loop - the deferred follow-up called out in `src/pages/login.rs`. A standalone session stores `{ access_token, refresh_token, expires_at, user }` in sessionStorage (`STANDALONE_KEY`) with `AuthContext.tokens = None`, so the OIDC loop skips it. When mokosh-server's 1-hour standalone access token expired, the next 401 hit `refresh_user_from_me`, which cleared the session - even though a valid 7-day (30-day remember-me) refresh token was sitting unused in sessionStorage. ## Fix (client-only) Add `use_standalone_token_refresh`, the standalone twin of `use_token_refresh`, mounted once at the app root (`main.rs`). Each 30s tick reloads the persisted `StandaloneSession` and, once the access token is within 60s of expiry, rotates it via `POST /api/v1/auth/refresh` (endpoint already exists, returns rotated tokens), persisting the result to sessionStorage and the fetch access-token holder. On any refresh failure it clears the session and redirects to /login, matching the OIDC error branch. - No-ops when no standalone session is stored; the OIDC loop still no-ops for standalone (`tokens = None`) - each hook owns exactly one session type. - Tokens stay in per-tab sessionStorage and the loop is a single root instance, so there is no refresh-token-reuse race (unlike the OIDC family-reuse footgun). - No server change: `POST /api/v1/auth/refresh` already rotates and returns `{ access_token, refresh_token, expires_at }`. ## Testing `cargo fmt`, `cargo clippy --all-targets -- -D warnings`, and `cargo check --target wasm32-unknown-unknown` all green in the pinned rust-builder image. ## Scope This addresses only the **standalone** logout. The separate **bunyip/OIDC** logout (~10 min, a failing OIDC refresh) is being pinned from live logs/console evidence and will get its own issue + fix.
fix(auth): silently refresh standalone (non-OIDC) sessions (MAPPS-374)
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 1m30s
Create release / Create release from merged PR (pull_request) Has been skipped
e5c2c3c459
The mokosh-apps SPA refreshed only OIDC sessions (use_token_refresh); the standalone (legacy mokosh-server email+password) path had no refresh loop, so its ~1h access token simply expired and the next 401 dropped the user to /login even though a valid 7-day refresh token sat unused in sessionStorage. This was the deferred follow-up called out in src/pages/login.rs.

Add use_standalone_token_refresh, the standalone twin of use_token_refresh, mounted once at the app root. Each 30s tick reloads the persisted StandaloneSession and, once the access token is within 60s of expiry, rotates it via POST /api/v1/auth/refresh, persisting the new tokens to sessionStorage (STANDALONE_KEY) and the fetch access-token holder. On any refresh failure it clears the session and redirects to /login, matching the OIDC error branch. It no-ops when no standalone session is stored, and the OIDC loop still no-ops for standalone (tokens = None), so each hook owns exactly one session type.

Client-only: POST /api/v1/auth/refresh already exists and returns rotated tokens. Tokens stay in per-tab sessionStorage and the loop is a single root instance, so there is no refresh-token-reuse race.

#MAPPS-374

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch fix/MAPPS-374-standalone-silent-refresh 2026-07-16 17:09:01 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!435
No description provided.