Skip to content

Rotate JWT at least daily (sleep-robust) - #97

Merged
dubadub merged 8 commits into
mainfrom
feat/daily-jwt-rotation
Jun 6, 2026
Merged

Rotate JWT at least daily (sleep-robust)#97
dubadub merged 8 commits into
mainfrom
feat/daily-jwt-rotation

Conversation

@dubadub

@dubadub dubadub commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

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_due decision 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 injected now for deterministic testing.
  • last_refresh persistence (src/auth/secure_session.rs): a unix-epoch timestamp stored in the keyring alongside the session, cleared on logout/delete.
  • Wiring (src/auth/mod.rs): set_session stamps last_refresh on every token acquisition (login + refresh); the hourly task now uses refresh_due and reads Utc::now() each tick.
  • Logout-safety fix: a transient refresh failure (network blip / 5xx) on a healthy token now keeps the session and retries next tick; clear_session() only fires when the token is genuinely near expiry. The invalid-JWT path is unchanged.
  • Observability: refresh-timestamp read/parse failures are now logged instead of silently swallowed.

Design & plan

  • Spec: docs/superpowers/specs/2026-06-06-daily-jwt-rotation-design.md
  • Plan: docs/superpowers/plans/2026-06-06-daily-jwt-rotation.md

Known 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 failures
  • cargo clippy --all-targets -- -D warnings -A dead_code — clean
  • cargo fmt --check — clean
  • Unit tests cover all refresh_due branches incl. the exact-24h boundary, plus last_refresh save/load/delete
  • After install, log shows JWT token refresh dueJWT token refreshed successfully ~daily instead of the prior 4-month silence

@dubadub
dubadub merged commit 46dfd8e into main Jun 6, 2026
2 checks passed
@dubadub
dubadub deleted the feat/daily-jwt-rotation branch June 6, 2026 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant