Back to Blog
Python

Handling Expired and Invalid Tokens with PyJWT in Python

python pyjwt expired and invalid token handling: Learn how to decode JWT tokens with PyJWT, catch expired and invalid token errors, and implement robust validation in...

PyJWTJWT validationtoken expirationerror handlingPython security
A digital lock with a clock and a warning triangle, representing expired and invalid JWT token handling in Python.

When a JWT expires or becomes malformed, PyJWT raises exceptions that your application must handle explicitly. The python pyjwt expired and invalid token handling pattern is straightforward once you understand the exception hierarchy and the difference between a token that is structurally invalid and one that is simply past its expiration time. This article shows how to decode tokens, catch the relevant errors, and build a validation flow that behaves correctly in production.

Understanding PyJWT's Exception Hierarchy

PyJWT groups all token-related failures under InvalidTokenError. This base exception covers expired tokens, bad signatures, malformed payloads, and missing required claims. The most important subclass for this topic is ExpiredSignatureError, which is raised when the exp claim is present and the current time is past that value.

import jwt from jwt import ExpiredSignatureError, InvalidTokenError

The hierarchy matters because catching InvalidTokenError alone will also catch ExpiredSignatureError. If you want to respond differently to an expired token (for example, by refreshing it) versus a genuinely invalid token (by rejecting it), you need to catch the specific exception first.

Decoding a Token and Catching Expired Errors

The typical flow is to decode the token with jwt.decode(), passing the secret key and the expected algorithms. If the token is expired, PyJWT raises ExpiredSignatureError before it checks the signature or other claims. This means you can catch that exception and decide whether to issue a new token or force re-authentication.

def validate_token(token, secret): try: payload = jwt.decode(token, secret, algorithms=["HS256"]) return payload except ExpiredSignatureError: # Token is past its expiration time raise TokenExpiredError("Token has expired") except InvalidTokenError as e: # Signature invalid, malformed, or missing required claims raise TokenInvalidError(str(e))

In this example, TokenExpiredError and TokenInvalidError are custom exceptions you define in your application. The key point is that ExpiredSignatureError is caught before the broader InvalidTokenError, so you can treat expiration as a distinct condition.

Handling Invalid Tokens and Distinguishing Error Types

Not every invalid token is expired. A token may have a bad signature, an incorrect iss claim, or a payload that fails structural validation. PyJWT raises different exceptions for these cases, but they all inherit from InvalidTokenError. To give the client a meaningful response, you should inspect the exception type when you need more detail.

try: payload = jwt.decode(token, secret, algorithms=["HS256"]) except jwt.ExpiredSignatureError: # 401 with a specific message return {"error": "token_expired"}, 401 except jwt.InvalidSignatureError: # Signature verification failed return {"error": "invalid_signature"}, 401 except jwt.DecodeError: # Token is not valid JSON or base64 return {"error": "malformed_token"}, 400 except jwt.InvalidTokenError: # Catch-all for any other token problem return {"error": "invalid_token"}, 401

This granularity is useful when your API needs to distinguish between a token that can be refreshed and one that must be rejected outright. However, for most applications, catching InvalidTokenError after ExpiredSignatureError is sufficient.

Checking Expiration Without Decoding the Full Token

Sometimes you only need to know whether a token is expired, without verifying its signature. You can decode the payload without verification using jwt.decode(..., options={"verify_signature": False}), but this is dangerous because the payload is untrusted. A safer approach is to extract the exp claim from the unverified payload and compare it to the current time, but you should never trust unverified claims for authorization decisions.

import time def is_token_expired(token): try: unverified_payload = jwt.decode(token, options={"verify_signature": False}) exp = unverified_payload.get("exp") if exp is None: return False # No expiration claim return time.time() > exp except jwt.InvalidTokenError: return True # Malformed token counts as expired

This pattern is useful for pre-checking a token before sending it to a resource server, but it should not replace proper verification. The only reliable way to know a token is valid is to verify its signature and all required claims.

Common Mistakes When Handling Expired Tokens

A frequent mistake is catching InvalidTokenError and treating it the same as an expired token. This can lead to refreshing tokens that are actually invalid, which extends a session that should have been terminated. Another mistake is ignoring the leeway parameter, which allows a small time window for clock skew between servers. PyJWT supports leeway in seconds:

payload = jwt.decode(token, secret, algorithms=["HS256"], leeway=30)

This gives a 30-second grace period after expiration. Use it when your services run on different machines with slightly different clocks, but keep it small to avoid unnecessarily long token lifetimes.

A third mistake is assuming that an expired token always raises ExpiredSignatureError. If the token is malformed or has an invalid signature, it will raise a different error even if the exp claim is in the past. Always catch the specific exception first.

Security Considerations for Token Validation

When handling expired and invalid tokens, the most important security rule is to never trust the payload without verifying the signature. An attacker can forge a token with a future exp claim if they know the secret, so signature verification is the foundation of token security. Additionally, always specify the algorithms parameter in jwt.decode(). Omitting it can allow algorithm confusion attacks, where an attacker signs a token with none or a different algorithm.

# Insecure: no algorithm restriction payload = jwt.decode(token, secret) # Secure: explicitly allow only HS256 payload = jwt.decode(token, secret, algorithms=["HS256"])

If your application uses asymmetric algorithms like RS256, ensure the public key is correctly loaded and the private key is never exposed. Token expiration is a defense against replay attacks, so do not disable it or set extremely long expiration times without a clear reason.

Production Considerations: Logging and Observability

In production, you should log token validation failures with enough context to debug issues without exposing sensitive data. Log the exception type and a truncated token identifier, but never log the full token or its payload. Use structured logging to make it easy to filter by error type.

import logging logger = logging.getLogger(__name__) def validate_token(token, secret): try: payload = jwt.decode(token, secret, algorithms=["HS256"]) return payload except ExpiredSignatureError: logger.warning("Token expired for user %s", payload.get("sub")) raise except InvalidTokenError as e: logger.info("Invalid token: %s", type(e).__name__) raise

This approach lets you monitor how often tokens expire versus how often they are rejected for other reasons. If you see a spike in ExpiredSignatureError, it may indicate that your token lifetime is too short or that clients are not refreshing tokens properly. A high rate of InvalidSignatureError could signal a misconfigured secret or an attempted attack.

For high-traffic services, consider caching the verification result for a short period if the same token is checked repeatedly, but be careful not to cache expired tokens. The safest pattern is to verify every request and let the exception handling decide the response.

python pyjwt expired and invalid token handling: Practical U | RYUSLOG DEV