Python PyJWT: Encode, Decode, and Verify JWT Tokens
python pyjwt encode decode and verify jwt tokens: Learn how to encode, decode, and verify JWT tokens with PyJWT in Python, including signature verification, error hand...
When you need to implement stateless authentication or signed data exchange in Python, PyJWT is the most widely used library for handling JSON Web Tokens. The core operations are encoding a token from a payload, decoding it back, and verifying that the signature is valid. This article walks through python pyjwt encode decode and verify jwt tokens using PyJWT's API, with attention to the details that matter in production: algorithm selection, expiration, and custom claim validation.
Encoding a JWT with PyJWT
Encoding a JWT with PyJWT is a one-line operation once you have a payload and a secret key. The jwt.encode() function takes three required arguments: the payload (a dictionary), the key (a string or bytes), and the algorithm to use for signing. The result is a compact, URL-safe string that can be sent to a client or stored in a cookie.
import jwt payload = { "sub": "1234567890", "name": "Jane Doe", "iat": 1516239022 } secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") print(token)
The payload is serialized to JSON, then base64url-encoded. The header, which contains the algorithm and token type, is also encoded and prepended. The signature is computed over the combined header and payload using the specified algorithm. The resulting token is a three-part string separated by dots.
PyJWT automatically adds the iat (issued at) claim if you include it, but it does not add any claims by default. You are responsible for including standard claims like exp (expiration), nbf (not before), or aud (audience) when your application requires them. The library will not enforce these claims unless you explicitly pass them during decoding.
Decoding a JWT and Verifying the Signature
Decoding a JWT in PyJWT is done with jwt.decode(). By default, this function verifies the signature and validates the exp and nbf claims if they are present. The algorithms parameter is required in recent versions of PyJWT to prevent algorithm confusion attacks. You must specify the list of algorithms you expect the token to be signed with.
import jwt token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" secret = "your-256-bit-secret" try: decoded = jwt.decode(token, secret, algorithms=["HS256"]) print(decoded) except jwt.InvalidTokenError as e: print(f"Invalid token: {e}")
The decode() function returns the payload as a dictionary. It verifies the signature using the provided key and the algorithm specified in the token's header. If the signature is invalid, it raises jwt.InvalidSignatureError. If the token is expired, it raises jwt.ExpiredSignatureError. Both are subclasses of jwt.InvalidTokenError, so catching the parent class covers all token-related failures.
PyJWT also supports decoding without signature verification, which is useful for debugging or for extracting the payload from an untrusted token before you decide how to handle it. This is done by passing options={"verify_signature": False}. Never use this in production for authentication; it bypasses the security guarantee that the token was issued by your server.
# WARNING: This does not verify the signature. unverified_payload = jwt.decode(token, options={"verify_signature": False})
Handling Expiration and Common Token Errors
Expiration is one of the most common claims you will encounter. When you include exp in the payload, PyJWT checks it during decoding. If the current time is past the expiration, jwt.decode() raises jwt.ExpiredSignatureError. To handle this gracefully, catch the specific exception and respond with an appropriate HTTP status code, such as 401 Unauthorized.
import jwt import datetime payload = { "sub": "1234567890", "exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1) } token = jwt.encode(payload, secret, algorithm="HS256") try: decoded = jwt.decode(token, secret, algorithms=["HS256"]) # token is valid and not expired except jwt.ExpiredSignatureError: # token has expired pass except jwt.InvalidTokenError: # any other error (bad signature, malformed token, etc.) pass
PyJWT also validates the nbf (not before) claim if present, raising jwt.ImmatureSignatureError when the current time is before the nbf timestamp. Other common errors include jwt.InvalidAlgorithmError when the token's algorithm does not match the algorithms list, and jwt.MissingRequiredClaimError when you require specific claims via the options parameter.
When you need to require the presence of certain claims, use the options parameter in decode(). For example, to require the sub claim and the exp claim, you can pass options={"require": ["sub", "exp"]}. This raises jwt.MissingRequiredClaimError if any required claim is absent.
Choosing the Right Signing Algorithm
PyJWT supports a wide range of algorithms, but the two most common are HS256 (HMAC with SHA-256) and RS256 (RSA signature with SHA-256). The choice affects how you manage keys and how the token can be verified.
HS256 uses a single shared secret for both signing and verification. The same secret must be kept on every server that needs to verify tokens. This is simple but requires that the secret is never exposed to clients. If a client obtains the secret, they can forge tokens. HS256 is appropriate for internal services or when the token is only verified by the same application that issued it.
RS256 uses a private key to sign and a public key to verify. The private key stays on the issuing server, while the public key can be distributed to any service that needs to verify tokens. This is the standard choice for multi-service architectures or when tokens are verified by third parties. RS256 is more secure in distributed environments because the private key is never shared.
| Algorithm | Key type | Use case |
|---|---|---|
| HS256 | Shared secret | Single service, internal APIs |
| RS256 | RSA key pair | Microservices, third-party verification |
| ES256 | EC key pair | Similar to RS256 but with smaller keys |
When you decode a token, you must specify the algorithms list. This list should contain only the algorithms you explicitly support. If you leave it empty, PyJWT will raise an error. The list prevents an attacker from switching the algorithm to none or to a weaker algorithm that you didn't intend to use.
Validating Custom Claims
Standard claims like sub, exp, and aud cover common requirements, but often you need to validate application-specific claims. For example, you might want to ensure that the token was issued for a particular user role or that it contains a specific permission. PyJWT does not validate custom claims automatically; you must do it manually after decoding.
import jwt token = "..." secret = "your-256-bit-secret" try: payload = jwt.decode(token, secret, algorithms=["HS256"]) # Custom validation if payload.get("role") != "admin": raise PermissionError("Insufficient role") if payload.get("tenant_id") is None: raise ValueError("Missing tenant_id") # Proceed with request except jwt.InvalidTokenError: # Handle invalid token pass
For more complex validation, you can define a function that takes the decoded payload and returns a boolean or raises an exception. This keeps the logic separate from the request handling and makes it testable.
def validate_custom_claims(payload): required = ["role", "tenant_id"] for claim in required: if claim not in payload: raise jwt.InvalidTokenError(f"Missing claim: {claim}") if payload["role"] not in {"admin", "user"}: raise jwt.InvalidTokenError("Invalid role") return True
Security Considerations for JWT Verification
JWT verification is only as secure as the key management and the validation logic around it. One common mistake is using the same secret for both signing and verification in a public-facing API. If the secret is embedded in a frontend application, anyone can extract it and forge tokens. Always keep signing keys server-side and never expose them to clients.
Another risk is algorithm confusion. If you accept tokens with the alg header set to none or to an algorithm you didn't intend, an attacker could forge a token with a valid signature. PyJWT mitigates this by requiring the algorithms list in decode(). Always specify the exact algorithms your application accepts, and never use algorithms=["HS256", "RS256"] unless you have a clear reason, because it can lead to confusion about which key to use.
For RS256, you must verify that the public key you use for decoding is indeed the correct one. If an attacker can replace the public key in your configuration, they can sign tokens with their own private key. Use environment variables or a secure configuration service to store public keys, and consider rotating keys periodically.
Finally, be aware that JWT is not encrypted by default. The payload is base64url-encoded, which is not the same as encryption. Anyone can decode the payload and read its contents. Do not put sensitive data in a JWT unless you also encrypt it. If you need confidentiality, use JWE (JSON Web Encryption) or an encrypted transport layer, but do not rely on JWT itself for secrecy.
Handling Token Expiration in Long-Running Requests
In a typical web application, a token is sent with each request and decoded immediately. However, there are scenarios where a token might be valid at the start of a long-running operation but expire before the operation completes. PyJWT validates expiration at decode time, so if you decode early and then perform a lengthy task, the token could become invalid mid-task. This is usually acceptable because the operation is already authorized, but if you need to enforce freshness, you can re-decode the token before critical actions.
For background jobs that process a batch of tokens, you might want to check the expiration claim manually without raising an exception. You can access the exp claim from the decoded payload and compare it to the current time. This gives you finer control over how to handle expired tokens without aborting the entire batch.
import jwt import datetime token = "..." secret = "..." try: payload = jwt.decode(token, secret, algorithms=["HS256"], options={"verify_exp": False}) # Now check expiration manually exp = payload.get("exp") if exp and datetime.datetime.fromtimestamp(exp, tz=datetime.timezone.utc) < datetime.datetime.now(datetime.timezone.utc): # Token is expired, handle accordingly pass else: # Token is still valid pass except jwt.InvalidTokenError: pass
Using options={"verify_exp": False} disables automatic expiration checking, allowing you to implement custom logic. This is useful when you need to differentiate between an expired token and a token that is otherwise invalid, or when you want to log the expiration time for auditing.