Skip to content

No JWT revocation mechanism — leaked or stale tokens remain valid for the full expiry window #26

Description

@abayomicornelius

There is no session/token revocation mechanism anywhere in the codebase. src/middleware/auth.rs::decode_jwt validates only the signature and expiry (validation.validate_exp = true) — there is no jti claim, no server-side blacklist/allowlist, and no way to invalidate a JWT before its natural exp. src/models/user.rs::JwtClaims (referenced from auth.rs) has just sub, email, iat, exp — no jti.

Why it matters

  • With jwt_expiry_hours defaulting to 24 (src/config.rs), a leaked/stolen JWT (e.g. exfiltrated via XSS on a frontend, or a device theft scenario) remains fully valid for up to 24 hours with no way for the user or an operator to revoke it — there is no logout endpoint, no "sign out of all devices" capability, and no password-change-invalidates-existing-tokens behavior anywhere in src/routes/auth.rs.
  • This is especially significant given what these tokens authorize: every mutating financial route (create_escrow, create_subscription, send_batch_payment, etc.) trusts AuthUser (derived purely from JWT validity) with no secondary check.
  • Historical evidence in this repo (src/commits/auth_revoke.rs — a non-functional decoy filename under src/commits/) suggests token revocation was previously attempted or planned but never landed in the real src/ tree — worth treating this as a known, previously-identified gap rather than a hypothetical one.

Reproduction / detection

  1. Log in via POST /api/auth/login, capture the issued JWT.
  2. Confirm there is no POST /api/auth/logout (or equivalent) route registered in src/routes/mod.rs.
  3. Confirm the captured JWT remains valid (accepted by AuthUser::from_request_parts) for its full exp window with no server-side action able to shorten that window.

Proposed fix sketch

  • Add a jti: Uuid claim to JwtClaims, generated at issue_jwt time.
  • Add a revoked_tokens(jti UUID PRIMARY KEY, revoked_at TIMESTAMPTZ) table (or a users.token_valid_after TIMESTAMPTZ column for the simpler "invalidate everything issued before time X" approach, which avoids needing to track every individual jti and is cheaper to check per-request — a single indexed column compare vs. a table lookup per request).
  • AuthUser::from_request_parts checks the claim's iat (or jti) against whichever revocation mechanism is chosen, on every request — this adds one DB round-trip (or lookup) to every authenticated request, so weigh a short-TTL in-memory cache of revoked/valid-after state (invalidated on writes) against the added latency of a DB check per request, especially since this path is hit by every protected route in the router.
  • Add POST /api/auth/logout (bump token_valid_after to now for the current user) and consider a "logout everywhere" variant.

Edge cases

  • Clock skew between the issuing server and whichever server validates later (relevant if this ever runs as multiple replicas) — token_valid_after comparisons need a small grace window to avoid rejecting a token issued microseconds before a revocation event due to clock drift.
  • If a revoked_tokens table approach is chosen instead of token_valid_after, it needs a cleanup job (delete rows past their original exp) or it grows unboundedly — cross-reference issue CORS layer defaults to fully open (any origin/method/header) with no production guard #24's observability spike, since "does the keeper-style background job pattern get reused for token cleanup too" is a related design question worth deciding once, not per-feature.

Testing strategy

  • Issue a token, revoke it (via whichever mechanism), and assert AuthUser::from_request_parts now rejects it even though it hasn't naturally expired.
  • Assert a token issued after a revocation event (for the same user) is still accepted — proving the revocation is scoped to "before time X," not "block this user forever."
  • Load-test the added per-request revocation check's latency impact, since it runs on every single authenticated request in the system.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26backendBackend service logicsecuritySecurity concernvery hardVery difficult / senior-level bounty issue

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions