fix(web): rotate JWT after tier-grant so the launcher unlocks without log-out (BUNYIP-226) #255

Merged
YousifShkara merged 1 commit from fix/BUNYIP-226-rotate-jwt-after-tier-grant into main 2026-06-26 11:24:59 +02:00
Owner

Reproduced on staging with yousif+6: signup -> onboarding -> verify-email -> /dashboard rendered Membership card as "Trial / 30 days" (fresh from /memberships/me) but the Applications grid rendered Mokosh "Locked / Not Available" until the user manually logged out and back in. Root cause: BUNYIP-221's maybe_grant_initial_tier flipped trial_ends_at on the DB row, but the user's existing session cookie carries a JWT minted at login, BEFORE the grant. The /applications endpoint reads has_member_access() off those stale claims (trial_ends_at = None) and locks the tile. authenticate()'s existing refresh-on-401 path doesn't fire because the JWT is still valid, just out of date on the membership claims.

Explicitly rotate the access token at the two web-side call sites that can trigger the BUNYIP-221 grant:

  • onboarding_post: after a successful update_profile, call auth_api::refresh and merge the rotated Set-Cookie headers into the existing redirect's cookie list. The user lands on /dashboard with a fresh JWT and the launcher reads the new claims on first paint.
  • verify_email: after a successful confirm_email_verification, call auth_api::refresh when the request carries a session cookie (skip for fresh-browser clicks where there is nothing to rotate). Render the celebration card through html_cookies + the public_shell directly so the rotated cookies attach to the response.

A refresh failure in either path is non-fatal: the underlying grant still landed in the DB, the user just keeps the stale JWT until something else triggers a refresh, which is no worse than the pre-BUNYIP-226 behavior.

#BUNYIP-226

Reproduced on staging with yousif+6: signup -> onboarding -> verify-email -> /dashboard rendered Membership card as "Trial / 30 days" (fresh from /memberships/me) but the Applications grid rendered Mokosh "Locked / Not Available" until the user manually logged out and back in. Root cause: BUNYIP-221's maybe_grant_initial_tier flipped trial_ends_at on the DB row, but the user's existing session cookie carries a JWT minted at login, BEFORE the grant. The /applications endpoint reads has_member_access() off those stale claims (trial_ends_at = None) and locks the tile. authenticate()'s existing refresh-on-401 path doesn't fire because the JWT is still valid, just out of date on the membership claims. Explicitly rotate the access token at the two web-side call sites that can trigger the BUNYIP-221 grant: - onboarding_post: after a successful update_profile, call auth_api::refresh and merge the rotated Set-Cookie headers into the existing redirect's cookie list. The user lands on /dashboard with a fresh JWT and the launcher reads the new claims on first paint. - verify_email: after a successful confirm_email_verification, call auth_api::refresh when the request carries a session cookie (skip for fresh-browser clicks where there is nothing to rotate). Render the celebration card through html_cookies + the public_shell directly so the rotated cookies attach to the response. A refresh failure in either path is non-fatal: the underlying grant still landed in the DB, the user just keeps the stale JWT until something else triggers a refresh, which is no worse than the pre-BUNYIP-226 behavior. #BUNYIP-226
fix(web): rotate JWT after tier-grant so the launcher unlocks without log-out (BUNYIP-226)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 27s
Check / fmt + clippy + build + tests (pull_request) Successful in 9m59s
Create release / Create release from merged PR (pull_request) Has been skipped
f4b71d556b
Reproduced on staging with yousif+6: signup -> onboarding -> verify-email -> /dashboard rendered Membership card as "Trial / 30 days" (fresh from /memberships/me) but the Applications grid rendered Mokosh "Locked / Not Available" until the user manually logged out and back in. Root cause: BUNYIP-221's maybe_grant_initial_tier flipped trial_ends_at on the DB row, but the user's existing session cookie carries a JWT minted at login, BEFORE the grant. The /applications endpoint reads has_member_access() off those stale claims (trial_ends_at = None) and locks the tile. authenticate()'s existing refresh-on-401 path doesn't fire because the JWT is still valid, just out of date on the membership claims.

Explicitly rotate the access token at the two web-side call sites that can trigger the BUNYIP-221 grant:

- onboarding_post: after a successful update_profile, call auth_api::refresh and merge the rotated Set-Cookie headers into the existing redirect's cookie list. The user lands on /dashboard with a fresh JWT and the launcher reads the new claims on first paint.
- verify_email: after a successful confirm_email_verification, call auth_api::refresh when the request carries a session cookie (skip for fresh-browser clicks where there is nothing to rotate). Render the celebration card through html_cookies + the public_shell directly so the rotated cookies attach to the response.

A refresh failure in either path is non-fatal: the underlying grant still landed in the DB, the user just keeps the stale JWT until something else triggers a refresh, which is no worse than the pre-BUNYIP-226 behavior.

#BUNYIP-226
YousifShkara deleted branch fix/BUNYIP-226-rotate-jwt-after-tier-grant 2026-06-26 11:24:59 +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!255
No description provided.