You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Log in via POST /api/auth/login, capture the issued JWT.
Confirm there is no POST /api/auth/logout (or equivalent) route registered in src/routes/mod.rs.
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.
There is no session/token revocation mechanism anywhere in the codebase.
src/middleware/auth.rs::decode_jwtvalidates only the signature and expiry (validation.validate_exp = true) — there is nojticlaim, no server-side blacklist/allowlist, and no way to invalidate a JWT before its naturalexp.src/models/user.rs::JwtClaims(referenced fromauth.rs) has justsub,email,iat,exp— nojti.Why it matters
jwt_expiry_hoursdefaulting 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 insrc/routes/auth.rs.create_escrow,create_subscription,send_batch_payment, etc.) trustsAuthUser(derived purely from JWT validity) with no secondary check.src/commits/auth_revoke.rs— a non-functional decoy filename undersrc/commits/) suggests token revocation was previously attempted or planned but never landed in the realsrc/tree — worth treating this as a known, previously-identified gap rather than a hypothetical one.Reproduction / detection
POST /api/auth/login, capture the issued JWT.POST /api/auth/logout(or equivalent) route registered insrc/routes/mod.rs.AuthUser::from_request_parts) for its fullexpwindow with no server-side action able to shorten that window.Proposed fix sketch
jti: Uuidclaim toJwtClaims, generated atissue_jwttime.revoked_tokens(jti UUID PRIMARY KEY, revoked_at TIMESTAMPTZ)table (or ausers.token_valid_after TIMESTAMPTZcolumn for the simpler "invalidate everything issued before time X" approach, which avoids needing to track every individualjtiand is cheaper to check per-request — a single indexed column compare vs. a table lookup per request).AuthUser::from_request_partschecks the claim'siat(orjti) 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.POST /api/auth/logout(bumptoken_valid_afterto now for the current user) and consider a "logout everywhere" variant.Edge cases
token_valid_aftercomparisons need a small grace window to avoid rejecting a token issued microseconds before a revocation event due to clock drift.revoked_tokenstable approach is chosen instead oftoken_valid_after, it needs a cleanup job (delete rows past their originalexp) 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
AuthUser::from_request_partsnow rejects it even though it hasn't naturally expired.