Skip to content

Token hardening: immediate revocation, boot-bound monotonic lifetime - #159

Open
bentumbler wants to merge 1 commit into
WLAN-Pi:devfrom
bentumbler:feature/token-hardening
Open

Token hardening: immediate revocation, boot-bound monotonic lifetime#159
bentumbler wants to merge 1 commit into
WLAN-Pi:devfrom
bentumbler:feature/token-hardening

Conversation

@bentumbler

Copy link
Copy Markdown
Contributor

Implements P2 of the MCP auth & transport plan (design/discussion doc, sections 1.2 defects 2–3 and 4.1 P2; the boot-bound/monotonic design refines the doc's original “enable exp validation” wording after the clock-skew history in ba79239 surfaced): make revocation and expiry actually work, without trusting a wall clock the device (no RTC) cannot guarantee.

What changed

Revocation takes effect immediately. verify_token's cache fast-path returned success without checking the revoked flag, and revoke_token never evicted the cache entry — a revoked token kept validating until an unrelated cache clear or service restart. revoke_token now evicts the token from the in-process cache. (Sufficient while gunicorn runs --workers 1; noted in-code that multiple workers would need a shared invalidation channel.)

Boot-bound monotonic token lifetime. exp validation was disabled deliberately (ba79239, clock-skew on RTC-less devices) leaving expiry to the hourly DB purge. Tokens now carry bid (kernel boot id), upt (uptime at issuance), and ttl claims. Within the issuing boot, age is measured against CLOCK_BOOTTIME, so setting the wall clock can never extend or resurrect a token — in either mode. exp/iat remain in the token as advisory/display values.

TOKEN_LIFETIME_MODE (config constant) selects cross-reboot policy:

  • boot_bound (default): tokens die with the boot they were issued in; a startup sweep deletes previous-boot rows (cleanup only — they can no longer verify regardless). Zero wall-clock reliance.
  • wall_clock_grace: previous-boot tokens stay valid until wall-clock expiry. While the clock still lags the token's issuance time (early boot, before NTP/fake-hwclock catch-up), verification returns a retryable Clock not yet synchronized rejection and nothing is purged; once the clock catches up the token verifies again.

Trusting the clock across reboots in grace mode is sound because setting time requires SSH/sudo — the same privilege that can run getjwt or read the HMAC secret, so clock manipulation gains an attacker nothing they don't already have. External API clients cannot set time.

Reviewer notes

  • Default choice to confirm: boot_bound means the WLAN Pi app's 7-day tokens die on every reboot (re-pair via SSH getjwt). wall_clock_grace preserves today's app UX exactly and accepts already-issued tokens (they predate bid and take the wall-clock path). Flipping the default is one line in config.py.
  • In boot_bound mode, upgrading invalidates all previously issued tokens once (no bid claim → previous-boot path).
  • A service restart (same boot id) invalidates nothing in either mode — test-pinned.
  • No DB schema change: everything rides in JWT claims; the startup sweep parses claims without signature verification (cleanup only).

Tests

tests/test_token_revocation.py — 12 tests against the real TokenManager + temp SQLite: immediate revocation (cache-hit and cold path; the cache-hit test fails on the unfixed code), previous-boot rejection, monotonic expiry on both verify paths, expire→reboot→clock-reset stays dead, boot sweep spares same-boot tokens and service restarts, and grace mode (accept within wall expiry, reject after, retryable clock-lag rejection, same-boot monotonic immunity to a 30-day clock jump).

Ran locally under the box's py3.9 venv (34 passed across the auth suites); the tree is py313-only so CI is the authoritative gate.

🤖 Generated with Claude Code

Two fixes to the token layer (P2 of the MCP auth plan):

Revocation now takes effect immediately. verify_token's cache fast-path
returned success without checking the revoked flag, and revoke_token never
evicted the cache entry, so a revoked token kept validating until an
unrelated cache clear or service restart. revoke_token now evicts the
token from the in-process cache (sufficient while gunicorn runs a single
worker).

Token lifetime is now enforced without trusting the wall clock. The device
has no RTC, so exp validation was disabled (ba79239) and expiry only bit
via the hourly DB purge. Tokens now carry bid (kernel boot id), upt
(uptime at issuance), and ttl claims; within the issuing boot, age is
measured against CLOCK_BOOTTIME, so setting the clock can never extend or
resurrect a token. exp/iat remain as advisory display values.

Across reboots, TOKEN_LIFETIME_MODE (config) selects the policy:
- boot_bound (default): tokens die with their boot; a startup sweep
  removes previous-boot rows as cleanup.
- wall_clock_grace: previous-boot tokens stay valid until wall-clock
  expiry; while the clock still lags the token's issuance time the token
  is rejected retryably ("Clock not yet synchronized"), never purged.
  Trusting the clock across reboots is sound because setting time
  requires the same SSH/sudo privilege that can mint tokens directly.

Note: in boot_bound mode, upgrading invalidates already-issued tokens
once (they predate the bid claim). wall_clock_grace accepts them until
wall-clock expiry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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