How to Fix JWT Signature Verification Failed: Blocking 'alg:none' Attacks and Missing 'exp' Claims
Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: Your JWT library rejected a token because the
algheader is"none"(unsigned) or theexpclaim is absent/past its timestamp — both are fatal verification failures. - How to fix it: Enforce an explicit algorithm allowlist (
RS256orES256), always setexpto a short TTL (≤15 min for access tokens), and reject any token that fails either check at the middleware layer. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your token-generation code and get a hardened diff output without sending secrets anywhere.
The Incident (What Does the Error Mean?)
Raw error thrown by most JWT libraries (jsonwebtoken, python-jose, nimbus-jose-jwt):
Error: json web token signature verification failed.
Algorithm 'none' is not supported, or 'exp' claim is missing/expired in the payload.
This fires on token validation, not generation. Two distinct failure modes collapse into this single error:
| Mode | Trigger | Immediate Consequence |
|---|---|---|
alg: none |
Token header declares no signing algorithm | Library refuses to verify — or worse, an older/misconfigured lib accepts it silently |
Missing/expired exp |
Payload has no exp field, or exp < Date.now()/1000 |
Token is treated as permanently valid or already dead |
In production, this surfaces as 401 Unauthorized cascading across every authenticated endpoint until the token is rotated or the library config is patched.
The Attack Vector / Blast Radius
The alg:none vector is a CVE-class exploit, not a misconfiguration.
The attack flow is textbook:
- Attacker intercepts or crafts a JWT with header
{"alg":"none","typ":"JWT"}. - They strip the signature segment entirely (token becomes
header.payload.). - A vulnerable library that doesn't enforce an algorithm allowlist accepts this token as valid because there's nothing to verify.
- Attacker sets
sub: admin,role: superuserin the payload. Full privilege escalation. Zero cryptographic resistance.
This is CVE-2015-9235 (jsonwebtoken) and its variants. It has been re-discovered in Go, Python, and Ruby JWT libraries repeatedly. If your library version predates 2018 and you haven't pinned an algorithm allowlist, assume you are vulnerable.
Missing exp blast radius:
A token without exp is an immortal credential. If it leaks — via logs, a misconfigured S3 bucket, a browser history entry — it is valid forever. No rotation, no revocation list covers this unless you've built a token denylist (most teams haven't).
How to Fix It (The Solution)
Basic Fix — Enforce Algorithm + Inject exp
- const token = jwt.sign({ sub: userId, role: userRole }, secretKey);
+ const token = jwt.sign(
+ { sub: userId, role: userRole },
+ privateKey,
+ { algorithm: 'RS256', expiresIn: '15m' }
+ );
- const decoded = jwt.verify(token, secretKey);
+ const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Never pass algorithms: ['none'] or omit the algorithms array on verify. Omitting it means the library falls back to trusting whatever alg the token header declares — attacker-controlled input.
Enterprise Best Practice — Middleware-Level Enforcement
Don't trust individual service implementations. Enforce at the gateway.
Node.js / Express middleware:
- app.use((req, res, next) => {
- const token = req.headers.authorization?.split(' ')[1];
- const user = jwt.verify(token, secret);
- req.user = user;
- next();
- });
+ import { expressjwt } from 'express-jwt';
+
+ app.use(
+ expressjwt({
+ secret: publicKey,
+ algorithms: ['RS256'], // explicit allowlist — 'none' is rejected hard
+ credentialsRequired: true,
+ requestProperty: 'auth',
+ })
+ );
+
+ // Global error handler for JWT failures
+ app.use((err, req, res, next) => {
+ if (err.name === 'UnauthorizedError') {
+ return res.status(401).json({ error: 'Invalid or expired token.' });
+ }
+ next(err);
+ });
Python (PyJWT):
- payload = jwt.decode(token, secret, algorithms=None)
+ payload = jwt.decode(
+ token,
+ public_key,
+ algorithms=["RS256"],
+ options={
+ "require": ["exp", "iat", "sub"], # fail if any are missing
+ "verify_exp": True,
+ },
+ )
Key rotation: Use asymmetric keys (RS256/ES256) so you can rotate the private signing key without redeploying every verifying service. Publish the public key via a /.well-known/jwks.json endpoint.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Semgrep rule — block alg:none and missing expiresIn at commit time:
# .semgrep/jwt-hardening.yaml
rules:
- id: jwt-sign-missing-expiry
patterns:
- pattern: jwt.sign($PAYLOAD, $KEY, ...)
- pattern-not: jwt.sign($PAYLOAD, $KEY, { ..., expiresIn: ... }, ...)
message: "jwt.sign() called without expiresIn. Token will never expire."
severity: ERROR
languages: [javascript, typescript]
- id: jwt-verify-no-algorithm-allowlist
patterns:
- pattern: jwt.verify($TOKEN, $KEY)
message: "jwt.verify() called without explicit algorithms array. Vulnerable to alg:none attack."
severity: ERROR
languages: [javascript, typescript]
Run in CI:
semgrep --config .semgrep/jwt-hardening.yaml --error src/
2. Dependency pinning + audit:
# Node — flag known-vulnerable JWT library versions
npm audit --audit-level=high
# Python
pip-audit --requirement requirements.txt
3. OPA / Envoy policy (service mesh layer):
If you're running Istio or Envoy, enforce JWT validation at the proxy level so no service ever receives an unverified token regardless of application code:
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-enforce-rs256
spec:
jwtRules:
- issuer: "https://auth.yourdomain.com"
jwksUri: "https://auth.yourdomain.com/.well-known/jwks.json"
# Istio rejects alg:none and missing exp by default when jwksUri is set
4. Token TTL policy in your IdP (Okta, Auth0, Keycloak):
- Access tokens: ≤ 15 minutes
- Refresh tokens: ≤ 24 hours with rotation enabled
- Set
exp,iat,nbf, andjtias required claims in your token policy —jtienables single-use revocation if you ever need it.