You've just implemented JWT authentication for your API. Tests pass, the frontend gets the token, everything seems fine. Then you accidentally modify the payload and send it back to the server. And the server accepts it. Without signature verification, any attacker can impersonate any user.
At Meteora Web, we see this mistake in projects we inherit: JWTs passed off as secure, but with no real check on the origin. We come from accounting and code: we know a bug in signature verification is like a hole in the ledger. It will surface eventually, and it will cost you.
In this guide, we'll show you how to sign and verify JWTs robustly, and especially which vulnerabilities to avoid. All with real code examples in PHP and Node.js, ready to copy and test.
How JWT Signing Works and Why It's Critical for Security
A JWT consists of three parts separated by dots: header, payload, signature. The signature guarantees integrity. Without it, anyone can modify the payload (e.g., change role from "user" to "admin") and the server won't notice.
The signature is generated by hashing header + payload + a secret key (HMAC) or a key pair (asymmetric). The server recalculates the signature with the same key and compares. If they don't match, the token is rejected.
Supported algorithms: a choice that makes all the difference
JWTs support many algorithms: HS256, HS384, HS512 (HMAC with SHA-2), RS256, RS384, RS512 (RSA with SHA-2), ES256, ES384, ES512 (ECDSA).
Sponsored Protocol
The choice between symmetric (HMAC) and asymmetric (RSA/ECDSA) depends on context:
- HMAC: same key for signing and verifying. Ideal when client and server are in the same domain (single application). The key must remain secret on the server.
- RSA/ECDSA: private key for signing, public key for verifying. Perfect if the token is issued by an authentication server and verified by different services (microservices, public APIs).
Common mistake: using HMAC with the same key for issuing and verifying, but exposing the key in a native client or JavaScript. Anyone extracts the key and generates valid tokens.
What Are the Most Common JWT Vulnerabilities
JWT vulnerabilities are not abstract: they are exploited every day in real attacks. Here are the most frequent ones.
Algorithm confusion attack
The attacker modifies the JWT header by setting "alg": "none". If the server doesn't explicitly check that the algorithm is among the allowed ones, it accepts a token without signature. A variant: switch from RS256 to HS256. If the server expects a public key (RSA) but receives HMAC, it uses the public key (often known) to verify the HMAC signature. The attacker signs the token with the public key and the server approves it.
How to protect: never accept the algorithm from input. Set a fixed whitelist in code.
Key confusion attack
Similar to the previous one, but focused on key swapping. If the server uses the same variable for HMAC key and public key, an attacker can force the use of a known public key to sign HMAC tokens.
Sponsored Protocol
Missing signature verification
It sounds trivial, but it happens. Libraries with API that return the payload directly without verifying the signature. For example, jwt.decode() in Python without specifying verify=True. In Node.js, use jwt.verify(), not jwt.decode().
Token replay and expiration not enforced
A valid token can be reused indefinitely if exp is not set and there's no blacklist for revoked tokens. Also, if nbf (not before) is not checked, future tokens are accepted.
How to Verify a JWT Signature in a Robust Way
Let's see concrete implementations. We start with the most common scenario: server issuing and verifying tokens (HMAC). Then we move to the asymmetric case.
PHP with firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// SIGN (issuing)
$key = 'a-very-long-random-secret-key!';
$payload = [
'sub' => 123,
'name' => 'Mario Rossi',
'iat' => time(),
'exp' => time() + 3600
];
$jwt = JWT::encode($payload, $key, 'HS256');
// VERIFY
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
print_r($decoded);
// If signature is invalid or token expired, JWT::decode throws an exception.
Attention: JWT::decode accepts an array of keys or a single Key object. Always specify the algorithm in the second parameter. Never use JWT::decode($jwt, $key) without Key, because it accepts any algorithm up to version 5.x (known vulnerability).
Sponsored Protocol
Node.js with jsonwebtoken
const jwt = require('jsonwebtoken');
// SIGN
const secret = 'a-very-long-secret-key';
const token = jwt.sign(
{ sub: 123, name: 'Mario Rossi' },
secret,
{ algorithm: 'HS256', expiresIn: '1h' }
);
// VERIFY
jwt.verify(token, secret, { algorithms: ['HS256'] }, (err, decoded) => {
if (err) {
console.error('Invalid token:', err.message);
return;
}
console.log(decoded);
});
Key rule: in the algorithms option, specify only the algorithms you accept. If you don't, the library accepts any algorithm by default, exposing you to the alg confusion attack.
Asymmetric case (RS256)
If you use a key pair, signing happens with the private key and verification with the public key. In PHP:
$privateKey = file_get_contents('/path/to/private.pem');
$publicKey = file_get_contents('/path/to/public.pem');
// SIGN
$jwt = JWT::encode($payload, $privateKey, 'RS256');
// VERIFY
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
In Node.js:
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Never mix symmetric and asymmetric keys in the same project. Use a single approach per domain.
Sponsored Protocol
How to Protect JWTs from Algorithm Confusion and Key Confusion Attacks
Defense is a combination of solid code and good architectural practices.
Fixed algorithm whitelist
In code, never read the algorithm from the token header to decide how to verify. Set a constant or environment variable with the list of accepted algorithms.
$allowed_algs = ['HS256']; // or ['RS256']
And always pass this list to the verification function.
Multi-level token validation
- Structure: verify the token has exactly three parts separated by dots.
- Header: check that
typisJWTandalgis in the whitelist. - Payload: verify standard claims (
exp,nbf,iat,iss,aud). - Signature: recalculate the signature with the key and expected algorithm.
Some libraries already handle this, but if you use a minimal library, implement these steps yourself.
Secure key management
- Never hardcode keys in the code. Use environment variables or a vault.
- Rotate keys periodically for HMAC or RSA/ECDSA pairs.
- For asymmetric keys, never expose the private key. The public key can be distributed, but verify its origin (certificate, JWKS endpoint).
Prevent token replay
Beyond a short exp (15-60 minutes), implement a revocation mechanism for blacklisted tokens (e.g., logout, password change). A simple approach: include a unique ID (jti) in the token and store revoked tokens in a database (Redis with TTL). On every request, check if jti is in the blacklist.
Sponsored Protocol
$blacklisted = $redis->get('blacklist:' . $decoded->jti);
if ($blacklisted) {
throw new Exception('Token revoked');
}
What to Do Next
We've seen that a JWT is only secure if the signature is verified with the right algorithm and the correct key. A single detail — a decode() instead of verify(), an unforced algorithm — can open the door to an attack.
Immediate actions to take today:
- Check your verification code: are you using
jwt.verify()orJWT::decodewithKey? If you use a library without specifying the algorithm, fix it now. - Add an algorithm whitelist in your authentication middleware. Don't let the token header decide.
- Implement a blacklist for revoked tokens with Redis or a database. Especially if you have active logout or frequent password changes.
- Review token expiration: too long
expincreases theft risk. Aim for 15-60 minutes for access tokens and use refresh tokens with rotation. - Perform a security audit of the entire JWT flow: from the issuing server to the clients receiving the token. At Meteora Web, we do this regularly on projects we handle. If you want a thorough check, contact us.
For a broader security picture, read our pillar guide on cryptography and data security and the guide on vulnerability scanning to find flaws in your infrastructure.