fix(oidc): persist tokens before navigating + router-push internal return_to #84

Merged
YousifShkara merged 1 commit from fix/auth-callback-persists-tokens-before-nav into main 2026-06-06 04:36:48 +02:00
Owner

mokosh-server's E2E suite started failing on [setup] capture bearer from the SPA login: after OIDC completed, the SPA landed on /dashboard, the router showed an extra OIDC handshake fire, and no request ever carried Authorization: Bearer. The setup's 30s poll for any Bearer-carrying request hit zero.

Two bugs, one fix on each axis:

AuthCallback never persisted the token bundle to sessionStorage. The component wrote auth.user, set the in-memory access-token holder, and then called location.set_href(return_to) for any absolute return_to value (which start_login("/dashboard") from the Login component triggers). A hard reload tore the in-memory AuthContext down between the write and the next boot, and rehydrate_from_storage found nothing because save_auth was only called inside use_token_refresh's 60s-later rotation path. The SPA booted unauthenticated, AuthGuard fired another start_login, and the loop is exactly what the test trail showed.

The original behaviour worked by accident: use_memberships_loader used to fire GET /v1/auth/memberships against bunyip at the same render cycle as the AuthCallback write, and that cross-origin call carried the Bearer header BEFORE the reload tore everything down. mokosh-apps PR #79 replaced that fetch with local synthesis (because bunyip's AuthenticatedUser extractor was not accepting OIDC at+jwt, see BUNYIP-55), so the accidental capture was gone and the underlying token-persistence bug surfaced.

Fix:

  • Save the full StoredTokens bundle to sessionStorage BEFORE any navigation. rehydrate_from_storage now finds the bundle on the very next boot, and the SPA is authenticated immediately regardless of whether AuthCallback router-pushed or hard-reloaded.
  • For internal return_to paths (starting with /, not //), prefer navigator.push(Dashboard) so the in-memory signal carries through without a reload at all. Cross-origin or otherwise unrecognised return_to values still fall back to set_href, but the sessionStorage save above now covers that path too.

The Dioxus router only knows about Route enum variants, so an internal path beyond /dashboard collapses to Dashboard for the moment. The set of post-login return_to paths in use today is { "", "/", "/dashboard" }; this collapse is a no-op for all of them. Expand the matrix when a new return-to target is introduced.

No server-side change. Test plan: rerun the mokosh-server E2E suite once this and the deploy land; setup should capture a bearer on the first /api/v1/* fetch the dashboard fires.

mokosh-server's E2E suite started failing on `[setup] capture bearer from the SPA login`: after OIDC completed, the SPA landed on /dashboard, the router showed an extra OIDC handshake fire, and no request ever carried `Authorization: Bearer`. The setup's 30s poll for any Bearer-carrying request hit zero. Two bugs, one fix on each axis: `AuthCallback` never persisted the token bundle to sessionStorage. The component wrote `auth.user`, set the in-memory access-token holder, and then called `location.set_href(return_to)` for any absolute return_to value (which `start_login("/dashboard")` from the Login component triggers). A hard reload tore the in-memory `AuthContext` down between the write and the next boot, and `rehydrate_from_storage` found nothing because `save_auth` was only called inside `use_token_refresh`'s 60s-later rotation path. The SPA booted unauthenticated, AuthGuard fired another start_login, and the loop is exactly what the test trail showed. The original behaviour worked by accident: `use_memberships_loader` used to fire `GET /v1/auth/memberships` against bunyip at the same render cycle as the AuthCallback write, and that cross-origin call carried the Bearer header BEFORE the reload tore everything down. mokosh-apps PR #79 replaced that fetch with local synthesis (because bunyip's `AuthenticatedUser` extractor was not accepting OIDC at+jwt, see BUNYIP-55), so the accidental capture was gone and the underlying token-persistence bug surfaced. Fix: - Save the full `StoredTokens` bundle to sessionStorage BEFORE any navigation. `rehydrate_from_storage` now finds the bundle on the very next boot, and the SPA is authenticated immediately regardless of whether AuthCallback router-pushed or hard-reloaded. - For internal return_to paths (starting with `/`, not `//`), prefer `navigator.push(Dashboard)` so the in-memory signal carries through without a reload at all. Cross-origin or otherwise unrecognised return_to values still fall back to `set_href`, but the sessionStorage save above now covers that path too. The Dioxus router only knows about `Route` enum variants, so an internal path beyond `/dashboard` collapses to `Dashboard` for the moment. The set of post-login return_to paths in use today is `{ "", "/", "/dashboard" }`; this collapse is a no-op for all of them. Expand the matrix when a new return-to target is introduced. No server-side change. Test plan: rerun the mokosh-server E2E suite once this and the deploy land; setup should capture a bearer on the first `/api/v1/*` fetch the dashboard fires.
fix(oidc): persist tokens before navigating + router-push internal return_to
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 40s
c75a6eb31e
mokosh-server's E2E suite started failing on `[setup] capture bearer from the SPA login`: after OIDC completed, the SPA landed on /dashboard, the router showed an extra OIDC handshake fire, and no request ever carried `Authorization: Bearer`. The setup's 30s poll for any Bearer-carrying request hit zero.

Two bugs, one fix on each axis:

`AuthCallback` never persisted the token bundle to sessionStorage. The component wrote `auth.user`, set the in-memory access-token holder, and then called `location.set_href(return_to)` for any absolute return_to value (which `start_login("/dashboard")` from the Login component triggers). A hard reload tore the in-memory `AuthContext` down between the write and the next boot, and `rehydrate_from_storage` found nothing because `save_auth` was only called inside `use_token_refresh`'s 60s-later rotation path. The SPA booted unauthenticated, AuthGuard fired another start_login, and the loop is exactly what the test trail showed.

The original behaviour worked by accident: `use_memberships_loader` used to fire `GET /v1/auth/memberships` against bunyip at the same render cycle as the AuthCallback write, and that cross-origin call carried the Bearer header BEFORE the reload tore everything down. mokosh-apps PR #79 replaced that fetch with local synthesis (because bunyip's `AuthenticatedUser` extractor was not accepting OIDC at+jwt, see BUNYIP-55), so the accidental capture was gone and the underlying token-persistence bug surfaced.

Fix:
- Save the full `StoredTokens` bundle to sessionStorage BEFORE any navigation. `rehydrate_from_storage` now finds the bundle on the very next boot, and the SPA is authenticated immediately regardless of whether AuthCallback router-pushed or hard-reloaded.
- For internal return_to paths (starting with `/`, not `//`), prefer `navigator.push(Dashboard)` so the in-memory signal carries through without a reload at all. Cross-origin or otherwise unrecognised return_to values still fall back to `set_href`, but the sessionStorage save above now covers that path too.

The Dioxus router only knows about `Route` enum variants, so an internal path beyond `/dashboard` collapses to `Dashboard` for the moment. The set of post-login return_to paths in use today is `{ "", "/", "/dashboard" }`; this collapse is a no-op for all of them. Expand the matrix when a new return-to target is introduced.

No server-side change. Test plan: rerun the mokosh-server E2E suite once this and the deploy land; setup should capture a bearer on the first `/api/v1/*` fetch the dashboard fires.
YousifShkara deleted branch fix/auth-callback-persists-tokens-before-nav 2026-06-06 04:36:48 +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!84
No description provided.