Skip to content

Token Versioning

Token versioning is a mechanism that allows immediate invalidation of all active access tokens for a user without maintaining a token blacklist.

Problem

JWT access tokens are stateless — once issued, they are valid until they expire. Revoking a refresh token in the database does not affect already-issued access tokens. This means that after logout-all, a user's access token remains valid until its natural expiry (up to 15 minutes).

Solution

Each user has a token_version integer column (default: 1). This value is embedded in every access token at login time. On every authenticated request, the version in the token is compared against the version in the database.

token.token_version == user.token_version  →  valid ✅
token.token_version != user.token_version  →  401 Unauthorized ❌

Invalidation

When POST /auth/logout-all is called:

1. user.token_version increments: 1 → 2
2. All refresh tokens are revoked in the database
3. All existing access tokens now carry version 1
4. version 1 != version 2 → all tokens immediately invalid

Normal Login (Multiple Devices)

token_version is only incremented on logout-all — not on login. This means multiple simultaneous sessions work correctly:

Device A — login → token_version: 1 → valid ✅
Device B — login → token_version: 1 → valid ✅

logout-all called:
  → token_version: 1 → 2

Device A → version 1 != 2 → 401 ❌
Device B → version 1 != 2 → 401 ❌

Device A logs in again → token_version: 2 → valid ✅

Implementation

users table

token_version INTEGER NOT NULL DEFAULT 1

Access token payload

{
  "sub": "user-uuid",
  "token_version": 1,
  ...
}

Validation in dependencies.py

token_version = payload.get("token_version")
if token_version is None or token_version != user.token_version:
    raise InvalidTokenException()

Increment on logout-all

def logout_all(self, user_id) -> None:
    user = self.user_repo.get_by_id_with_roles(user_id)
    if user:
        self.user_repo.increment_token_version(user)
        self.token_repo.revoke_all_for_user(user_id)

Trade-offs

Aspect Token Blacklist Token Versioning
Granularity Per token Per user (all tokens)
DB queries 1 extra per request 0 extra (user already loaded)
Invalidation Immediate, per token Immediate, all tokens
Complexity Requires cleanup job Simple integer increment

Token versioning is the right choice when the requirement is "logout from all devices" rather than "logout from this specific device". For single-device logout, simply revoking the refresh token and waiting for the access token to expire naturally is sufficient.