How to Fix a JWT Verification Bug (verify_signature: False)
Setting verify_signature: False in PyJWT turns off the entire point of a JWT - every token authenticates, including ones signed with a random key or using the alg=none exploit. The fix is two lines, but only if you know the pattern.
What verify_signature: False actually does
PyJWT lets you decode without verifying for debugging purposes. When you pass:
jwt.decode(token, options={"verify_signature": False})
PyJWT skips all signature checks and returns the payload for any token - one
signed with a random key, a token from a different service, and critically the
alg=none forgery where no signature exists at all. If this is in your verify_jwt()
function, authentication is completely bypassed.
The fix: always decode with the secret and algorithm list
Replace the unsafe call with a real decode that enforces the signature and pins the
accepted algorithm to HS256:
import jwt
SECRET = "your-signing-key" # in production: load from env or KMS
def verify_jwt(token: str):
try:
return jwt.decode(token, SECRET, algorithms=["HS256"])
except jwt.PyJWTError:
return None
Three things are doing the work here:
SECRETas the second argument - PyJWT verifies the HMAC signature against this key; a forged token signed with any other key fails immediately.algorithms=["HS256"]- the allowlist blocks thealg=noneattack, where an attacker strips the signature and sets the header tonone. Without this list PyJWT may accept it.except jwt.PyJWTError- expired tokens, malformed tokens, bad signatures, and wrong algorithms all raise a subclass ofPyJWTError. Catching the base class returnsNoneinstead of letting the exception propagate and 500 the endpoint.
Why alg=none is dangerous
The alg=none exploit lets an attacker craft a token with a valid-looking payload,
set the header to {"alg": "none"}, and send it with an empty signature. Libraries
that do not enforce an algorithm allowlist will accept it as valid. This is a
well-documented class of vulnerability that has affected production systems - the
only defense is an explicit algorithms= list.
What production JWT validation also checks
The snippet above handles signature verification. A production verify_jwt should
also validate:
exp(expiry) - PyJWT checks this automatically; make sure your tokens include it.iss/aud(issuer / audience) - pass them as kwargs:jwt.decode(token, SECRET, algorithms=["HS256"], audience="my-api", issuer="auth.example.com").- Asymmetric keys (RS256 / ES256) - share the public key, not a symmetric secret, so the signing key never leaves the auth service. Load from a JWKS endpoint.
- Key rotation - rotating the signing key invalidates all outstanding tokens; plan for a short-lived transition window.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Replacing verify_signature: False with a real signature check using jwt.decode + secret + algorithms list
- Blocking the alg=none JWT attack with an explicit algorithms allowlist
- Catching jwt.PyJWTError to handle expired, malformed, and forged tokens safely
FAQ
Why does verify_signature: False bypass authentication?
It tells PyJWT to skip the HMAC check entirely - the library just decodes the payload without checking whether the signature matches your secret. Any token, including alg=none forgeries and tokens signed by attackers, will pass.
What is the alg=none JWT attack?
An attacker crafts a token with a valid-looking payload, sets the header algorithm to 'none', and omits the signature. Libraries without an algorithm allowlist accept it as valid. Passing algorithms=['HS256'] to jwt.decode blocks this attack.
How do I handle expired or malformed JWTs in Python?
Wrap jwt.decode in try/except jwt.PyJWTError - PyJWT raises a subclass of that base exception for expired tokens, bad signatures, wrong algorithms, and malformed tokens. Return None (or raise an HTTP 401) in the except block.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →