# What invariants bound session-token scope to the permission revocation window? **Date of Record:** 2026 November 2 **Scope:** Applies to session tokens (bearer tokens, session cookies, capability tokens) whose embedded permissions remain authoritative until token expiry, independent of the user's current permissions. Does not apply to per-request authorization checks against a live permission store. ## Core Distinction A session token is a snapshot of permissions at issuance time. Revocation updates the live permission store but does not reach already-issued tokens. The window between revocation and token expiry is a window in which the token grants permissions the principal no longer holds. **Invariant.** A token's effective permission set is bounded by the revocation window if and only if `token_ttl <= revocation_propagation_window`, OR the authorization path checks a revocation list on each request. **Violation.** The **Stale Grant**. A principal is demoted at t=0; their token was issued at t=−1800 with TTL 3600. The token continues to authorize privileged operations until t=1800, an hour after demotion. Revocation is recorded, correct, and ineffective for the token's remaining life. **Asymmetry.** Revocation is recorded and the permission store is accurate. The token is valid and correctly signed. The permission check passes because the token says so. The permission is wrong, but no system in the authorization path observes the current state. **Recorded elements.** The `token-issuance-receipt` (what permissions, what issuance time), the `revocation-propagation-signature` (how revocation reaches authorization), and the `token-ttl-receipt` (the token's lifetime) together make the bound checkable. ## What Was Observed **Signature.** Operations succeeding under a token whose principal has been demoted. The permission store shows demotion. The operation log shows the operation succeeded with the demoted principal's identity. Audit finds the two contradict. **Trace.** Admin role granted at t=−1800, token issued with TTL 3600. Admin demoted at t=0. Token remains valid until t=1800. At t=600, admin operation succeeds under the token. The operation is authenticated correctly (token signature valid) and authorized incorrectly (token claims a permission the principal no longer holds). **Why monitoring stays green.** Authentication succeeds. Authorization succeeds per the token. Revocation is recorded. No path compares current permissions against token-embedded permissions. ## Condition of Validity The instance must ensure at least one holds before it treats revocation as effective: 1. **Must bound TTL by propagation.** `token_ttl <= revocation_propagation_window` — the token's lifetime is no longer than the time revocation takes to reach every authorization path. 2. **Must check revocation on each request.** The authorization path checks a revocation list or version, so revocation is effective before token expiry. 3. **Must rotate on permission change.** Any change to a principal's permissions invalidates existing tokens, forcing re-issuance against current state. ## Decision Boundary - TTL bounded by propagation window: revocation takes effect within the bound. - Revocation checked per-request: revocation is immediate regardless of TTL. - Neither: stale grant is guaranteed for the remaining token lifetime. - Permission change rotates token: sound; requires token invalidation infrastructure. ## Operational Effect The invariant couples a token's embedded claims to a permission store that changes independently. The instance's correct posture is to bound TTL by propagation or to check revocation on each request. **Trap.** Shorten TTL. Shorter TTL narrows the window but does not close it, and it increases re-issuance load, which is itself a failure mode at scale. The correct fix is revocation-on-request or explicit rotation on permission change. ```python from dataclasses import dataclass class StaleGrantError(RuntimeError): """Raised when a token's TTL exceeds the revocation propagation window and no per-request check exists.""" @dataclass(frozen=True) class SessionTokenGate: token_ttl_seconds: int revocation_propagation_seconds: int per_request_revocation_check: bool rotate_on_permission_change: bool = False def verify(self) -> bool: if self.per_request_revocation_check or self.rotate_on_permission_change: return True if self.token_ttl_seconds > self.revocation_propagation_seconds: raise StaleGrantError( f"token TTL {self.token_ttl_seconds}s > revocation propagation " f"{self.revocation_propagation_seconds}s: demoted principal retains permissions" ) return True def dispatch_token_mode(gate: SessionTokenGate) -> str: try: gate.verify() except StaleGrantError: return "check-revocation-per-request" return "token-authoritative" ```