Security Error Guide: JWT 'invalid signature' / signature verification failed
Fix JWT 'invalid signature' and 'signature verification failed': diagnose wrong key, algorithm mismatch, key rotation/kid, encoding, and clock/claim confusion.
- #security
- #hardening
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
A JWT invalid signature error means the token’s payload is intact and readable, but the signature does not verify against the key and algorithm the validator used. The verifier recomputes the signature over the header and payload and compares it to the one in the token; a mismatch means the token was signed with a different key, a different algorithm, or its bytes changed in transit. This is the authentication control doing its job — a token that fails signature verification must be rejected, never trusted “anyway.”
The wording varies by library:
JsonWebTokenError: invalid signature
jwt: signature is invalid
jwt.exceptions.InvalidSignatureError: Signature verification failed
This is not the same as token expired (a valid signature but a stale exp claim) or token malformed (not a parseable JWT at all). invalid signature means the token parsed fine but cryptographic verification failed — treat every such token as untrusted.
Symptoms
- Tokens issued by one service are rejected by another that should trust them.
- Verification breaks right after a signing-key rotation or a config/deploy change.
- The same token verifies in one environment (staging) and fails in another (prod).
- Decoding the payload (without verifying) works and shows sensible claims, yet verification fails.
- Some tokens verify and others do not, correlating with the header’s
kid(key ID).
# decode header + payload WITHOUT verifying (inspection only)
TOKEN='eyJhbGciOi...'; for part in 1 2; do \
echo "$TOKEN" | cut -d. -f$part | tr '_-' '/+' | base64 -d 2>/dev/null; echo; done
{"alg":"RS256","typ":"JWT","kid":"2024-05"}
{"sub":"svc-billing","iss":"https://auth.internal.example.com","exp":1751808000}
Reading the header tells you which alg and kid the verifier must use — the usual source of the mismatch.
Common Root Causes
1. Wrong verification key
The verifier is configured with a different secret (HMAC) or public key (RSA/EC) than the one that signed the token.
# fetch the issuer's current public keys (JWKS)
curl -s https://auth.internal.example.com/.well-known/jwks.json | jq '.keys[] | {kid, kty, alg}'
{ "kid": "2024-05", "kty": "RSA", "alg": "RS256" }
If the token’s kid is not among the JWKS keys, the verifier is using a key that never signed this token.
2. Algorithm mismatch (and the alg-confusion trap)
The token is signed with RS256 (asymmetric) but the verifier is configured for HS256 (symmetric), or vice versa. Accepting whatever alg the token declares is also a known vulnerability — the verifier must pin an allow-list.
echo "$TOKEN" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
{"alg":"HS256","typ":"JWT"}
An HS256 header hitting an RS256-only verifier (or the reverse) fails signature verification — and pinning the expected algorithm is the correct defense, not loosening it.
3. Key rotation / stale kid
The issuer rotated its signing key, but the verifier still holds the old public key (cached JWKS) and cannot match the new kid.
curl -s https://auth.internal.example.com/.well-known/jwks.json | jq -r '.keys[].kid'
echo "$TOKEN" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null | jq -r '.kid'
2026-07
2024-05
The token’s kid (2024-05) is no longer in the live JWKS (2026-07) — the verifier’s key cache is stale.
4. Secret/key encoding or whitespace drift
An HMAC secret was copied with a trailing newline, base64-decoded when it should be raw (or vice versa), or a PEM key lost its header/footer or gained CRLF line endings.
# compare the exact byte length of the configured secret across environments
printf %s "$JWT_SECRET" | wc -c
# check a PEM key parses cleanly
openssl rsa -pubin -in jwt-public.pem -noout 2>&1 || openssl pkey -pubin -in jwt-public.pem -noout
44
unable to load Public Key
A differing byte count between environments, or a PEM that fails to load, points at an encoding/whitespace problem rather than the wrong key.
5. Token altered or truncated in transit
A proxy, logger, or copy-paste truncated the token or mangled the URL-safe base64, so the signature no longer matches the payload.
echo -n "$TOKEN" | awk -F. '{print NF" segments; sig length="length($3)}'
3 segments; sig length=342
Two segments, an empty third segment, or a signature length far from the algorithm’s norm means the token was corrupted before it reached the verifier.
6. Multi-issuer / wrong tenant key
The service accepts tokens from several issuers/tenants and selected the wrong issuer’s key because it did not match on iss + kid.
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq '{iss, kid: input_line_number}'
{ "iss": "https://auth.tenant-b.example.com" }
If the verifier used tenant-A’s key for a tenant-B token, the signature will never match — key selection must be driven by the verified iss/kid.
Diagnostic Workflow
Step 1: Confirm it is signature, not expiry or malformed
Decode the payload (inspection only) and check the failure wording: invalid signature / signature verification failed is this guide; exp/expired is a clock/claim issue; malformed/invalid token is a parsing issue.
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq '{iss, exp, iat}'
Step 2: Read the header’s alg and kid
echo "$TOKEN" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null | jq '{alg, kid}'
These two fields decide which key and algorithm the verifier must use.
Step 3: Match the kid against the issuer’s live keys
curl -s "$(echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq -r '.iss')/.well-known/jwks.json" \
| jq -r '.keys[].kid'
If the token’s kid is absent from the live JWKS, refresh the verifier’s key cache.
Step 4: Verify against the intended key explicitly
# RS256 example: verify with the fetched public key
python3 - <<'PY'
import jwt # PyJWT
token=open('/tmp/token.txt').read().strip()
key=open('jwt-public.pem').read()
print(jwt.decode(token, key, algorithms=['RS256'], options={"verify_aud": False}))
PY
A clean decode with the correct key confirms the earlier failure was a wrong-key/algorithm/rotation issue.
Step 5: Rule out encoding and truncation
echo -n "$TOKEN" | awk -F. '{print NF" segments"}'
printf %s "$JWT_SECRET" | wc -c # HMAC: compare across environments
Confirm three segments and that any shared secret has an identical byte length everywhere.
Example Root Cause Analysis
After the identity service completed a scheduled signing-key rotation, one downstream API began rejecting every token from a long-running batch client:
jwt.exceptions.InvalidSignatureError: Signature verification failed
The payload decodes fine and exp is in the future, so this is signature verification, not expiry. Reading the header shows the key the token expects:
echo "$TOKEN" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null | jq '{alg, kid}'
{ "alg": "RS256", "kid": "2026-07" }
But the API’s cached JWKS still lists only the pre-rotation key:
curl -s https://auth.internal.example.com/.well-known/jwks.json | jq -r '.keys[].kid' # live
cat /var/cache/api/jwks.json | jq -r '.keys[].kid' # cached
2026-07
2024-05
The issuer rotated to kid=2026-07, but the API pinned a stale JWKS and had no refresh, so it verified new tokens against the old key and every check failed. The correct fix is to refresh the JWKS (and honor cache-control / rotate on kid miss) so the verifier always has the current public keys — not to disable signature verification or hard-code a single key.
# after refreshing the JWKS cache
python3 -c "import jwt,urllib.request,json; \
jwks=json.load(urllib.request.urlopen('https://auth.internal.example.com/.well-known/jwks.json')); \
print('kids:', [k['kid'] for k in jwks['keys']])"
kids: ['2026-07']
With the current key present, verification succeeds and rotation no longer breaks trust.
Prevention Best Practices
- Verify tokens against a JWKS endpoint and refresh keys automatically (honor cache TTL and re-fetch on a
kidmiss) so signing-key rotation never requires a redeploy. - Pin an explicit algorithm allow-list on the verifier (e.g.
algorithms=['RS256']) — never accept the token’s declaredalg, which enables the RS256/HS256 confusion attack. - Select the verification key from the verified
iss+kid, not from a single hard-coded key, in any multi-issuer/multi-tenant service. - Store HMAC secrets and PEM keys as exact bytes (watch for trailing newlines, base64-vs-raw, and CRLF); compare byte length across environments when a token verifies in one and fails in another.
- Never “fix” an
invalid signatureby disabling verification, decoding without a key, or settingverify=False— a token that fails signature verification is untrusted by definition. See the security hardening guides for a token-validation baseline. - When a rotation or deploy triggers a wave of signature failures, the free incident assistant can distinguish a stale-JWKS, algorithm-mismatch, or wrong-tenant-key cause from the header and JWKS output.
Quick Command Reference
# Signature vs expiry vs malformed? (payload inspection only)
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq '{iss, exp}'
# Which alg / kid must the verifier use?
echo "$TOKEN" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null | jq '{alg, kid}'
# Is that kid in the issuer's LIVE keys?
curl -s https://<ISSUER>/.well-known/jwks.json | jq -r '.keys[].kid'
# Segment / signature sanity (rule out truncation)
echo -n "$TOKEN" | awk -F. '{print NF" segments; sig len="length($3)}'
# HMAC secret byte length (compare across environments)
printf %s "$JWT_SECRET" | wc -c
Conclusion
A JWT invalid signature is an authentication failure on a token that parsed correctly — the signature does not match the key and algorithm the verifier used. Work it from the header outward:
- Confirm it is signature verification, not expiry or a malformed token.
- Read the
algandkidfrom the header — they dictate the key and algorithm. - Match the
kidagainst the issuer’s live JWKS; refresh a stale key cache. - Pin the expected algorithm and avoid the RS256/HS256 confusion trap.
- Rule out encoding drift, truncation, and wrong-tenant key selection.
Fix the key, algorithm, or rotation handling — and never disable signature verification to make the error disappear, because that turns off authentication itself.
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.