Rotate JWT at least daily (sleep-robust) - #97
Merged
Conversation
Add REFRESH_KEY constant, save_last_refresh/load_last_refresh helpers (production + _with_store + _with_mock variants), and clear it in delete_with_store. Tested via MockKeyring with 3 new unit tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The daemon only refreshed the JWT within 1 hour of expiry (
should_refresh()), and the server issues long-lived (~14-day) tokens. Investigating the production log showed zero refresh events over a 4-month span — if the laptop is asleep during that single pre-expiry hour, the token expires and the user is forced to re-login.This change makes the daemon attempt a refresh at least every 24 hours, decided by wall-clock elapsed time so machine sleep can't skip the window. With daily attempts against a ~14-day token, the laptop would have to sleep for many consecutive days to lose the session.
What changed
refresh_duedecision helper (src/auth/jwt.rs): pure function — refresh when near expiry (existing safety net), when the last-refresh time is unknown, or when ≥24h have elapsed. Takes an injectednowfor deterministic testing.last_refreshpersistence (src/auth/secure_session.rs): a unix-epoch timestamp stored in the keyring alongside the session, cleared on logout/delete.src/auth/mod.rs):set_sessionstampslast_refreshon every token acquisition (login + refresh); the hourly task now usesrefresh_dueand readsUtc::now()each tick.clear_session()only fires when the token is genuinely near expiry. The invalid-JWT path is unchanged.Design & plan
docs/superpowers/specs/2026-06-06-daily-jwt-rotation-design.mddocs/superpowers/plans/2026-06-06-daily-jwt-rotation.mdKnown trade-off (follow-up)
An explicitly revoked token that still has >1h of life would be retried hourly rather than forcing immediate re-login, until its own expiry. A tighter fix is to branch on HTTP status from the refresh call (401/403 → clear, 5xx/network → keep); deferred as it needs the error type to expose the status.
Test plan
cargo test— 95 unit + 12 integration, 0 failurescargo clippy --all-targets -- -D warnings -A dead_code— cleancargo fmt --check— cleanrefresh_duebranches incl. the exact-24h boundary, pluslast_refreshsave/load/deleteJWT token refresh due→JWT token refreshed successfully~daily instead of the prior 4-month silence