Authentication is one of the easiest things to mess up when building modern APIs. Many engineering teams implement JSON Web Tokens (JWTs) to build stateless authentication systems, but end up introducing critical vulnerabilities into their applications.
Here are three common JWT security mistakes developers make in production and how to fix them.
1. Storing JWTs in LocalStorage
The most widespread architectural mistake on the modern web is storing authentication tokens inside browser localStorage or sessionStorage.
// DANGEROUS: Storing sensitive access tokens in LocalStorage
localStorage.setItem('accessToken', token);
The Vulnerability
localStorage is completely accessible to any JavaScript running on your domain. If your application suffers from a Cross-Site Scripting (XSS) vulnerability, an attacker can execute malicious scripts to read localStorage and exfiltrate your users' authentication tokens.
A single rogue dependency in your node_modules bundle or an unsanitized user input field can expose every active session in your application.
The Fix: HttpOnly Cookies
Store access tokens inside HttpOnly cookies instead. When a cookie has the HttpOnly flag enabled, client side JavaScript physically cannot read or extract it.
// SECURE: Setting an HttpOnly, Secure cookie in Express.js
res.cookie('token', jwtToken, {
httpOnly: true, // Prevents client side JS access
secure: true, // Ensures cookie is sent over HTTPS only
sameSite: 'strict', // Protects against Cross-Site Request Forgery (CSRF)
maxAge: 15 * 60 * 1000 // Short expiration (15 minutes)
});
The browser automatically attaches this cookie to every outgoing request to your API domain, keeping the token invisible to malicious scripts.
2. Using decode() Instead of verify()
A JWT consists of three base64 encoded parts: the header, the payload, and the signature. Anyone can decode a JWT payload without a secret key.
// DANGEROUS: Decoding the payload without verifying the signature
const decoded = jwt.decode(token);
const userId = decoded.userId;
The Vulnerability
The decode() method simply parses the base64 string. It does not check whether the token was altered by an attacker. If a user modifies their payload to change their role from user to admin, jwt.decode() will accept the forged payload without hesitation.
The Fix: Cryptographic Verification
You must always use jwt.verify() with a secure, server side secret key before trusting any data inside the payload.
// SECURE: Verifying token signature with your secret key
try {
const verifiedPayload = jwt.verify(token, process.env.JWT_SECRET);
req.user = verifiedPayload;
} catch (error) {
res.status(401).json({ message: 'Invalid or tampered token' });
}
If an attacker tampers with a single character in the payload, the signature check fails and the request is rejected immediately.
3. Weak Signing Secrets
When using symmetric algorithms like HMAC SHA-256 (HS256), the security of your authentication relies entirely on the strength of your secret key.
// DANGEROUS: Weak secret keys vulnerable to brute force attacks
const token = jwt.sign(payload, "my_super_secret_key_123");
The Vulnerability
Because JWTs are stored on the client side, an attacker who obtains a valid token can run offline brute force attacks against it. Tools like Hashcat can test millions of potential secret keys per second against your token signature. If your secret is simple, an attacker will crack it in seconds, allowing them to forge valid administrative tokens at will.
The Fix: High Entropy Secrets
Always generate long, cryptographically secure random secrets for signing production tokens. You can easily generate a strong 256-bit secret key in Node.js using the built in crypto module:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Store this value safely inside an environment variable and never commit it to source control.
The Verdict
API security requires defense in depth. Stop storing tokens in localStorage, never trust unverified payloads, and use cryptographically strong secret keys.
How does your team currently store authentication tokens on the frontend? Let us discuss in the comments below.
Top comments (0)