Python PyJWT Expiration and Custom Claims
python pyjwt expiration and custom claims: Learn how to set and verify expiration and custom claims with PyJWT in Python, including error handling and security conside...
python pyjwt expiration and custom claims requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When working with JSON Web Tokens in Python, PyJWT is the most common library for encoding and decoding tokens. The exp claim controls token expiration, and custom claims let you attach application-specific data to the payload. This article covers how to set expiration and custom claims with PyJWT, how to verify them on decode, and what to watch for in production.
How PyJWT Handles the exp Claim
PyJWT follows the JWT specification for registered claims. The exp claim is a Unix timestamp that marks the moment after which the token is no longer valid. When you decode a token, PyJWT automatically checks exp if it is present in the payload. If the current time is past the exp value, decoding raises jwt.ExpiredSignatureError. This check happens before you can access any custom claims, so you need to handle it explicitly.
Setting Expiration When Encoding a Token
To set the expiration, include exp in the payload dictionary when calling jwt.encode. The value must be an integer Unix timestamp or a datetime object. PyJWT converts a datetime to a timestamp automatically.
import jwt from datetime import datetime, timedelta, timezone payload = { "user_id": 42, "role": "admin", "exp": datetime.now(timezone.utc) + timedelta(hours=1) } token = jwt.encode(payload, "secret-key", algorithm="HS256")
The exp claim is now part of the encoded token. When the token is decoded later, PyJWT will compare the current time against this value.
Adding Custom Claims to the Payload
Custom claims are simply additional keys in the payload dictionary. They are not verified by PyJWT automatically; you must check them yourself after decoding. Common custom claims include user roles, permissions, tenant IDs, or any application-specific data that should travel with the token.
payload = { "user_id": 42, "role": "admin", "tenant": "acme-corp", "exp": datetime.now(timezone.utc) + timedelta(hours=1) }
Keep custom claims small and non-sensitive. The payload is base64-encoded, not encrypted, so anyone with the token can read the claims. Do not store passwords or personal data in custom claims.
Verifying Expiration and Custom Claims on Decode
When decoding, PyJWT checks exp by default. If the token is expired, it raises jwt.ExpiredSignatureError. You can catch this exception and return an appropriate response. For custom claims, you need to validate them manually after a successful decode.
try: decoded = jwt.decode(token, "secret-key", algorithms=["HS256"]) except jwt.ExpiredSignatureError: # Token is expired; return 401 or refresh token raise except jwt.InvalidTokenError: # Token is malformed or signature invalid raise # Validate custom claims if decoded.get("role") != "admin": raise PermissionError("Insufficient role")
The algorithms parameter is required in PyJWT 2.x to prevent algorithm confusion attacks. Always specify the exact algorithm you expect.
Handling Expired Tokens and Invalid Claims
In a typical web application, an expired token should trigger a 401 response and a token refresh flow. The ExpiredSignatureError is a subclass of InvalidTokenError, so you can catch it separately to return a specific error message. For custom claims, decide what happens when a required claim is missing or has an unexpected value. You might reject the request or fall back to a default permission level.
def decode_token(token): try: decoded = jwt.decode(token, "secret-key", algorithms=["HS256"]) except jwt.ExpiredSignatureError: return None, "Token expired" except jwt.InvalidTokenError: return None, "Invalid token" if "tenant" not in decoded: return None, "Missing tenant claim" return decoded, None
This pattern keeps the token validation logic in one place and avoids duplicating checks across handlers.
Security and Operational Considerations for exp and Custom Claims
The exp claim is only as reliable as the server clock. If your servers run with skewed clocks, tokens may expire earlier or later than intended. Use NTP to keep clocks synchronized, and consider adding a small leeway when decoding to tolerate clock drift. PyJWT does not add leeway by default; you can pass the leeway parameter to jwt.decode to allow a few seconds of grace.
decoded = jwt.decode(token, "secret-key", algorithms=["HS256"], leeway=10)
Custom claims are not encrypted. If you need to protect sensitive data, use a different mechanism or encrypt the payload. Also, avoid putting mutable data like permissions in custom claims if you need to revoke them immediately. A token with a long exp will keep the old permissions until it expires. For fine-grained access control, consider shorter token lifetimes or a token revocation list.
Choosing Between exp and Other Time-Based Claims
The JWT specification also defines nbf (not before) and iat (issued at). nbf specifies the earliest time the token is valid, and iat records when it was issued. PyJWT verifies nbf if present, but iat is not validated by default. Use exp for the primary expiration mechanism. Add nbf if you need to issue tokens that become valid later, for example in scheduled operations. Keep iat for informational purposes or audit trails.
payload = { "user_id": 42, "nbf": datetime.now(timezone.utc) + timedelta(minutes=5), "exp": datetime.now(timezone.utc) + timedelta(hours=1), "iat": datetime.now(timezone.utc) }
When decoding, PyJWT will enforce nbf and exp automatically, but you should still handle the corresponding exceptions.