Skip to content
Ashish.
All posts
Diagram illustrating JWT statelessness and the concept of token revocation via a denylist.
6 min readBackendBackend DevelopersFeatured#jwt#security#authentication#backend#token-revocation#denylist#api-security

How to Revoke a JWT: Denylist & Expiry Strategies

Learn how to revoke a JWT you already issued using denylists and token invalidation strategies for secure backend authentication.

By Ashish KumarPart 10 of JWT From the Spec Up

Revoking a JWT You Already Issued

JSON Web Tokens (JWTs) are architected to be stateless, allowing servers to validate credentials without maintaining session data. This design simplifies horizontal scaling but creates a critical security gap: once issued, a JWT remains valid until its expiration claim (exp) is reached, even if the user logs out or the token is compromised.

Because JWTs lack a native revocation mechanism, enforcing immediate invalidation requires breaking the stateless contract. This guide explores two primary strategies for revoking JWTs: implementing a denylist (blocklist) using external state stores like Redis, and utilizing short-lived access tokens paired with refresh token rotation.

The Statelessness Paradox

The core tension in JWT security lies between convenience and control. When a backend receives a JWT, it typically performs two validations:

  1. Signature Verification: Ensuring the token has not been tampered with by verifying the cryptographic signature against the secret or public key.
  2. Claim Validation: Checking that the exp (expiration) time is in the future and that other claims (e.g., iss, aud) are valid.

If both checks pass, access is granted. The JWT specification (RFC 7519) does not define a standard field for real-time revocation. Consequently, a stolen or logged-out token cannot be rejected by the server alone unless an external mechanism intercepts the validation process. This shifts the authentication model from purely stateless to hybrid stateful.

Security best practices, as outlined in the OWASP JWT Security Cheat Sheet and vendor documentation from providers like Auth0 and AWS Cognito, emphasize that relying solely on the exp claim is insufficient for sensitive applications. These authorities recommend implementing explicit revocation mechanisms or strict token lifetime limits to mitigate the risks associated with stateless validation.

The Denylist (Blocklist) Mechanism

The most direct method for revoking a JWT is maintaining a denylist. This involves tracking identifiers of tokens that should no longer be accepted.

How It Works

  1. Issue Token: Generate a unique jti (JWT ID) or use the token's hash as the key when issuing the JWT.
  2. Store in Denylist: Upon logout or security event, add the jti to a fast key-value store such as Redis.
  3. Check on Access: Middleware intercepts protected requests, checks if the jti exists in the denylist, and returns 401 Unauthorized if found.

Implementation Example

The following Python example demonstrates checking a Redis store for revoked tokens during validation.

import redis
import jwt
 
redis_client = redis.Redis(host='localhost', port=6379, db=0)
 
def verify_token(token):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        jti = payload.get('jti')
        
        # Check if the token is in the denylist
        if redis_client.exists(f"denylist:{jti}"):
            raise Exception("Token revoked")
            
        return payload
    except jwt.ExpiredSignatureError:
        raise Exception("Token expired")

Critical Considerations

  • TTL Alignment: The denylist entry must have a Time-To-Live (TTL) equal to the JWT's expiration time. If a JWT expires in 15 minutes, the denylist entry should expire in 15 minutes. Without this, the denylist grows indefinitely, leading to memory exhaustion.
  • Latency: Adding a Redis lookup to every request increases latency. In high-throughput systems, this can become a bottleneck. Local caching of denylist checks is possible but generally discouraged for security-critical authentication due to consistency risks.
  • Performance: Use O(1) operations like EXISTS or GET in Redis. Avoid scanning the entire denylist, which degrades performance as the list grows.

Short-Lived Tokens & Refresh Rotation

An alternative to explicit denylisting is minimizing the window of vulnerability by issuing short-lived access tokens paired with longer-lived refresh tokens.

How It Works

  1. Access Token: Short-lived (e.g., 5–15 minutes). Used for API requests.
  2. Refresh Token: Long-lived (e.g., 7 days). Stored securely (e.g., HTTP-only cookie). Used to obtain new access tokens.
  3. Rotation: Every time a refresh token is used, the server issues a new refresh token and invalidates the old one.

Revocation via Refresh Token

If a user logs out or a breach is detected:

  1. Invalidate the current refresh token in the database or Redis.
  2. The attacker retains the valid access token, but it will expire in minutes.
  3. When the attacker attempts to refresh the access token, the server rejects the invalid refresh token.

This approach reduces the impact of a stolen access token significantly, though it does not provide instant revocation of the access token itself. The window of vulnerability is limited strictly to the access token's lifetime.

Implementation Example

The following example shows how to handle refresh token rotation and invalidation.

def refresh_access_token(refresh_token):
    # Verify refresh token signature
    payload = jwt.decode(refresh_token, REFRESH_SECRET, algorithms=["HS256"])
    jti = payload.get('jti')
    
    # Check if refresh token is still valid in DB
    if not db.is_refresh_token_valid(jti):
        raise Exception("Refresh token revoked")
        
    # Issue new access and refresh tokens
    new_access_token = create_access_token(payload['sub'])
    new_refresh_token = create_refresh_token(payload['sub'])
    
    # Invalidate old refresh token
    db.invalidate_refresh_token(jti)
    
    return {
        "access_token": new_access_token,
        "refresh_token": new_refresh_token
    }

Trade-offs and Recommendations

StrategyProsCons
DenylistInstant revocation. Simple concept.Requires external state (Redis). Adds latency to every request. Memory overhead.
Short-Lived TokensNo immediate revocation needed. Stateless access.Complex implementation (rotation). Window of vulnerability exists.
Hybrid ApproachBest of both worlds.Most complex.

For most backend applications, a hybrid approach offers the best balance:

  1. Use short-lived access tokens (5–15 minutes) to limit the impact of theft.
  2. Use refresh token rotation with a denylist for refresh tokens only.
  3. If immediate access token revocation is required (e.g., password change), use a denylist for access tokens but limit the scope (e.g., only block specific user sessions).

Key Takeaways

  • JWTs cannot be revoked natively; you must introduce state.
  • A denylist (blocklist) is the most direct revocation mechanism but adds latency and storage overhead.
  • Short-lived tokens with refresh rotation reduce the impact of stolen tokens but do not provide instant revocation.
  • Always align the TTL of denylist entries with the JWT expiration time to prevent memory leaks.

Common Pitfalls

When implementing JWT revocation, developers often encounter several recurring issues that can compromise security or system stability:

  1. Forgetting to Set TTL on Denylist Entries: As mentioned, if you add a token to a denylist but do not set an expiration time, the list will grow indefinitely. This leads to memory exhaustion and potential service crashes. Always ensure the denylist key expires at the same time the JWT does.
  2. Using Weak Hashing for JTI: If you use a hash of the entire JWT as the denylist key, ensure you use a strong cryptographic hash function (like SHA-256). Weak hashing can lead to collisions, potentially allowing a revoked token to be accepted or causing unnecessary cache evictions.
  3. Storing Sensitive Data in JWT Claims: JWTs are encoded, not encrypted. Storing sensitive information (PII, passwords, credit card numbers) in claims is a severe security risk, as anyone can decode them without any key, since the payload is only base64url-encoded. Only store non-sensitive identifiers like user IDs or roles.

FAQ

Can I revoke a JWT without a database? Not truly. By definition, revocation requires checking a state. However, you can effectively "revoke" a token by letting it expire quickly (short TTL). If you need immediate revocation without a persistent store, you would need an in-memory cache (like Redis or Memcached) on the server, which still constitutes external state.

What is the difference between a blocklist and a denylist? There is no technical difference. "Denylist" and "blocklist" are synonyms used interchangeably in security contexts. Both refer to a list of identifiers (such as JTI values or user IDs) that are explicitly rejected by the authentication middleware.

How long should a JWT live? For access tokens, it is generally recommended to keep them short-lived, typically between 5 to 15 minutes. This minimizes the window of opportunity for an attacker if the token is stolen. Refresh tokens can have longer lifespans (e.g., days or weeks), but they should also be subject to rotation and revocation policies.

Conclusion

Revoking a JWT requires moving beyond the stateless paradigm. By implementing a denylist or leveraging short-lived tokens with refresh rotation, developers can enforce security policies that protect against compromised credentials. Choose the strategy that best aligns with your application's performance requirements and security posture.

Related posts