fix(web): rotate at+jwt when /users/me reports a different role than the cookie's JWT #308
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/bunyip!308
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/BUNYIP-308-rotate-jwt-on-role-mismatch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A user who just becomes an admin on bunyip could not see any admin data (audit-log history, users list, admin feedback, ...) until they manually logged out and back in. Root cause:
AdminUser(crates/bunyip-domain/src/middleware/auth.rs:203-224) gates every/v1/admin/*endpoint onclaims.roledecoded from the incoming at+jwt, NOT the DB row. bunyip-web'sguard/admin_guardreadrolefrom the DB-fresh/users/meresponse, so the admin shell renders happily; but the SAME cookie's at+jwt still carries the pre-promotionrole = "subscriber"claim, so every/v1/admin/*fetch on the shell 403s and the pages come back empty.The intended cure is BUNYIP-137 (revoke refresh tokens on role change) + BUNYIP-145 (fan out
SessionRevokedvia SSE) forcing the tab to reload through /login. That works when SSE reaches the browser. In practice it does not always: BUNYIP-307 documented that bunyip-web on c-01 was serving the internal docker origin into the SSE subscriber, so the browser blocked the whole connection as Mixed Content and the revoke event was never delivered. Even after BUNYIP-307 restores SSE, an unlucky user can be mid-request when the promotion lands, or on a route without SSE mounted, and still see the same broken-admin state.Close the gap in
bunyip-web/src/auth.rs::authenticate. After/users/mereturns 200 with the DB-fresh User, decode theroleclaim (unverified) from the cookie's at+jwt payload and compare it against the DB role. On mismatch, call/auth/refreshbefore returning; bunyip-api's refresh path re-fetches the user before minting (services/auth.rs:404-437), so the rotated at+jwt carries the current role. Merge the returned Set-Cookies into the request's forward cookie so the SAME request's downstream/v1/admin/*fetches see the fresh claim, and pass the Set-Cookies up to the browser so the next request also carries the rotated token. Refresh failure is not fatal - fall back to the pre-refresh AuthCtx and let bunyip-api's 401/403 route the user through the normal /login path.The unverified decode is safe here: bunyip-api's
/users/mehas already verified the token against its keys, so we know the token is trusted. We only decode the payload as data to detect a stale-role mismatch. A malformed / unparseable token yields None and skips the mismatch check, which preserves the pre-BUNYIP-308 behaviour.No change to the
AdminUser/MemberUserextractors, no change to/v1/users/me, no change to the SSEsession_revoked/claims_changedhandlers, no change to the docker configs. The whole fix lives inside one BFF function plus a tiny helper.New base64 dep on bunyip-web (0.21, same version already used across bunyip-domain / bunyip-oidc / bunyip-api). Unit tests cover: admin/subscriber decode from a well-formed cookie, missing
access_tokencookie, malformed JWT, payload with norolefield, role_matches on the promotion + demotion + unknown cases.just check-containerclean (224 domain tests pass).Covers both directions. Promotion: DB=admin, JWT=subscriber -> rotate, fresh admin JWT lands, admin shell fetches succeed. Demotion: DB=subscriber, JWT=admin -> rotate. If the demoted user's session was revoked by BUNYIP-137, refresh fails and the user's next click lands them at /login (correct semantics).
#BUNYIP-308