fix(launcher): close the cross-browser stale-JWT gap on the app launcher (BUNYIP-229) #256

Merged
YousifShkara merged 1 commit from fix/BUNYIP-229-stale-jwt-cross-browser into main 2026-06-29 05:02:46 +02:00
Owner

Reproduced on staging post-BUNYIP-226 with yousif+7: signup in Browser A, name-save in A, verify email by opening the link in Browser B, refresh A → /dashboard rendered Mokosh Locked again. BUNYIP-226's refresh-at-grant-site pattern only covered the same-browser shape: in this cross-browser flow the BUNYIP-221 grant fires in Browser B's session (no cookie there, refresh skipped) while Browser A holds the stale JWT, and the path Browser A takes after refresh (onboarding_get → /dashboard forward) had no rotation trigger.

Two layered fixes:

  1. onboarding_get now calls auth_api::refresh before redirecting to /dashboard when needs_onboarding returns false. Any user landing on /onboarding after the dual gate has been satisfied gets their JWT rotated before the next hop, so the dashboard reads the fresh claims on first paint. Mirrors the BUNYIP-226 pattern on the GET-side forwarder. Refresh failure is non-fatal.

  2. /v1/applications and /v1/applications/{slug} now read has_member_access from the DB user row instead of trusting the JWT claim cache. The launcher is the surface where the staleness symptom is visible, and the JWT can go stale via any of: cross-browser email verify firing the grant, an admin tier flip, a scheduled trial expiry, the BUNYIP-225 sibling-sub cleanup. A one-row DB read per dashboard load is cheap and works regardless of which handler caused the flip - the "always works" backstop so the next state-flipping path nobody remembered to wire to refresh() doesn't reintroduce this surprise. Anonymous callers and DB read failures fall back to the existing claim path.

#BUNYIP-229

Reproduced on staging post-BUNYIP-226 with yousif+7: signup in Browser A, name-save in A, verify email by opening the link in Browser B, refresh A → /dashboard rendered Mokosh Locked again. BUNYIP-226's refresh-at-grant-site pattern only covered the same-browser shape: in this cross-browser flow the BUNYIP-221 grant fires in Browser B's session (no cookie there, refresh skipped) while Browser A holds the stale JWT, and the path Browser A takes after refresh (onboarding_get → /dashboard forward) had no rotation trigger. Two layered fixes: 1. onboarding_get now calls auth_api::refresh before redirecting to /dashboard when needs_onboarding returns false. Any user landing on /onboarding after the dual gate has been satisfied gets their JWT rotated before the next hop, so the dashboard reads the fresh claims on first paint. Mirrors the BUNYIP-226 pattern on the GET-side forwarder. Refresh failure is non-fatal. 2. /v1/applications and /v1/applications/{slug} now read has_member_access from the DB user row instead of trusting the JWT claim cache. The launcher is the surface where the staleness symptom is visible, and the JWT can go stale via any of: cross-browser email verify firing the grant, an admin tier flip, a scheduled trial expiry, the BUNYIP-225 sibling-sub cleanup. A one-row DB read per dashboard load is cheap and works regardless of which handler caused the flip - the "always works" backstop so the next state-flipping path nobody remembered to wire to refresh() doesn't reintroduce this surprise. Anonymous callers and DB read failures fall back to the existing claim path. #BUNYIP-229
fix(launcher): close the cross-browser stale-JWT gap on the app launcher (BUNYIP-229)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 1m17s
Check / fmt + clippy + build + tests (pull_request) Successful in 29m58s
Create release / Create release from merged PR (pull_request) Has been skipped
383b6aee51
Reproduced on staging post-BUNYIP-226 with yousif+7: signup in Browser A, name-save in A, verify email by opening the link in Browser B, refresh A → /dashboard rendered Mokosh Locked again. BUNYIP-226's refresh-at-grant-site pattern only covered the same-browser shape: in this cross-browser flow the BUNYIP-221 grant fires in Browser B's session (no cookie there, refresh skipped) while Browser A holds the stale JWT, and the path Browser A takes after refresh (onboarding_get → /dashboard forward) had no rotation trigger.

Two layered fixes:

1. onboarding_get now calls auth_api::refresh before redirecting to /dashboard when needs_onboarding returns false. Any user landing on /onboarding after the dual gate has been satisfied gets their JWT rotated before the next hop, so the dashboard reads the fresh claims on first paint. Mirrors the BUNYIP-226 pattern on the GET-side forwarder. Refresh failure is non-fatal.

2. /v1/applications and /v1/applications/{slug} now read has_member_access from the DB user row instead of trusting the JWT claim cache. The launcher is the surface where the staleness symptom is visible, and the JWT can go stale via any of: cross-browser email verify firing the grant, an admin tier flip, a scheduled trial expiry, the BUNYIP-225 sibling-sub cleanup. A one-row DB read per dashboard load is cheap and works regardless of which handler caused the flip - the "always works" backstop so the next state-flipping path nobody remembered to wire to refresh() doesn't reintroduce this surprise. Anonymous callers and DB read failures fall back to the existing claim path.

#BUNYIP-229
YousifShkara deleted branch fix/BUNYIP-229-stale-jwt-cross-browser 2026-06-29 05:02:46 +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/bunyip!256
No description provided.