Skip to content
Ashish.
All posts
Diagram illustrating the JWT JTI claim in replay protection and token revocation.
6 min readSecurityBackend Engineers, Security EngineersFeatured#jwt#jti#security#replay-protection#token-revocation#backend#authentication

JWT JTI: Replay Protection and Token Revocation

Learn how the JWT JTI claim enables effective replay protection and token revocation strategies for backend security.

By Ashish KumarPart 4 of JWT From the Spec Up

The Stateless Paradox: Why JWTs Need State

JSON Web Tokens (JWTs) are designed to be stateless. When a backend receives a JWT, it validates the signature and claims locally, without contacting a database or session store. This design offers incredible scalability: any server in a cluster can validate any token independently. However, this statelessness creates a fundamental security gap known as the "revocation problem."

If a user’s account is compromised, or a secret key is leaked, you need to invalidate existing tokens immediately. With traditional sessions, you simply delete the session record. With JWTs, the token remains cryptographically valid until its exp (expiration) claim is reached. If that exp is set to 24 hours, an attacker has a 24-hour window of unrestricted access.

The jti (JWT ID) claim, defined in RFC 7519, is the primary mechanism for bridging this gap. It provides a unique identifier for each token, allowing the backend to maintain a minimal state record (a list of issued or revoked IDs) to enforce security policies that pure statelessness cannot handle.

The JTI Claim: A Unique Fingerprint

The jti claim is a string value that uniquely identifies the JWT. The intent of the jti is to provide a unique value for a single token, as defined in RFC 7519. If two tokens have the same jti, they are considered the same token for the purpose of validation logic, even if their signatures are different (though valid signatures should never collide).

Consider a backend issuing an access token for a user alice@example.com.

{
  "sub": "alice@example.com",
  "iat": 1516239022,
  "exp": 1516242622,
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Here, jti acts as a primary key in a temporary lookup table. Without this identifier, the backend has no way to distinguish between "Token A issued at 10:00 AM" and "Token B issued at 10:01 AM" if they have the same subject (sub) and expiration. With jti, the backend can track exactly which specific token instances are active.

Mechanism: Replay Protection via Deduplication

A replay attack occurs when an attacker intercepts a valid JWT and replays it to gain unauthorized access. In a stateless system, once the token is signed, it is valid until it expires. jti enables replay protection by allowing the backend to reject tokens that have already been processed.

How It Works

  1. Issuance/Validation: When the backend receives a token with a jti, it checks a storage layer (e.g., Redis) to see if this jti has been seen before.
  2. First Use: If the jti is not found, the backend processes the request and stores the jti in the storage layer with a TTL (Time-To-Live) matching the token's exp claim. This ensures the record is automatically cleaned up after the token expires.
  3. Replay Detection: If the attacker replays the same token, the backend finds the existing jti record and rejects the request with a 401 Unauthorized or 409 Conflict.

The Windowed Approach

Strict deduplication (blocking all repeats) is often too aggressive for real-world systems due to network retries and client-side caching. A more common mechanism is windowed deduplication.

The backend stores the jti only for a short window after issuance (e.g., 5 minutes). If a token is presented within that window, it is checked against the cache. If it’s outside the window, the backend trusts the exp claim and signature. This balances security against performance overhead.

Token Revocation: The Denylist Strategy

jti is also critical for token revocation. When a security event occurs (e.g., password change, logout), you cannot change the signature of existing tokens. Instead, you must invalidate them.

The Denylist (Revocation List)

The most direct application of jti is maintaining a denylist. When a user logs out or is compromised, the backend adds the jti of their current token to a "blacklist" in Redis or a database.

# Pseudocode for revocation
def revoke_token(jti, token_exp_timestamp):
    # Calculate remaining lifetime until token expiration
    ttl = token_exp_timestamp - current_time()
    redis_client.set(f"revoked:{jti}", "true", ex=ttl)

When a subsequent request arrives, the middleware checks:

  1. Is the jti in the revoke list?
  2. If yes, reject.
  3. If no, proceed with signature validation.

Scaling Challenges

Storing every revoked jti indefinitely is unsustainable. A large enterprise might issue millions of tokens daily. Therefore, jti-based revocation is typically combined with short-lived access tokens (e.g., 15 minutes). The revoke list only needs to hold entries for the duration of the token’s life. Once the token expires, the jti entry naturally falls out of the cache, freeing memory.

Trade-offs and Implementation Details

Using jti introduces complexity that contradicts the "stateless" ideal of JWTs.

  1. Storage Overhead: You must maintain a persistent store (Redis, DynamoDB, etc.) for jti values. This adds latency to every request (network round-trip to check the cache) and cost (storage fees).
  2. Consistency: In a distributed system, cache consistency is vital. If one node accepts a token because it doesn’t have the jti in its local cache, but another node has revoked it, you have a security hole. Use centralized caching (like Redis Cluster) rather than local in-memory caches for jti tracking.
  3. Refresh Token Flow: jti is most effective when used with Refresh Tokens. Access tokens are short-lived and frequently refreshed. When a refresh token is used to get a new access token, the old access token’s jti can be revoked immediately. This limits the window of opportunity for replay attacks on access tokens to near-zero.

Conclusion

The jti claim is not a silver bullet, but it is a necessary component of secure JWT implementation. It transforms JWTs from purely stateless artifacts into components that can participate in stateful security policies. By providing a unique identifier for each token, jti enables replay protection through deduplication and allows for precise, granular token revocation. For backend engineers, implementing jti tracking is a critical step in moving from "theoretical security" (cryptographic signatures) to "practical security" (lifecycle management).

Related posts