Is JWT Secure? 7 Mistakes That Break It

By Bug Circuit Security Team
Is JWT Secure? 7 Mistakes That Break It

Is your own website affected? Check it free in 2 minutes.

Passive outside check only. Nothing is attacked, no signup.

Yes, a JWT can be secure, but only if you set it up correctly, because JWT security problems usually come from configuration mistakes and not from the format itself.

This guide is for developers and site owners who use JSON Web Tokens (JWTs, said "jots") to keep people logged in. You will get seven common mistakes, a quick way to spot each one in your own code, and a short fix for each. Plan on about 20 minutes to review your setup, using the checklist below.

What a JWT is, in plain English

A JWT is a small piece of text your server hands to a user after login. It has three parts separated by dots: a header, a payload (the data, such as a user ID), and a signature. The signature proves your server made the token and that nobody changed it.

Two facts drive everything below. First, the payload is only encoded, not encrypted, so anyone holding the token can read it. Second, your server must check the signature correctly every single time. Most of the mistakes below break one of those two ideas.

The 7 mistakes at a glance

Use this table as a quick review. The details follow.

#MistakeWhat an attacker can doQuick fix
1Weak signing secretGuess it offline, then forge tokensUse 32+ random bytes
2Leaked secretForge tokens for any userKeep it out of code and git, rotate it
3The none algorithm or unpinned algorithmSend an unsigned or confused tokenPin the allowed algorithm
4Skipped signature checkEdit the payload freelyUse verify, never just decode
5No expiryReuse a stolen token foreverShort exp plus refresh
6Token in localStorageSteal it with one XSS bugHttpOnly cookie
7Sensitive data in the payloadRead it without any keyStore only IDs and roles

1. A weak or guessable secret key

Many apps sign tokens with the HS256 algorithm (HMAC with SHA-256). That uses one shared secret. If the secret is secret, password123 or the app name, an attacker can take any token they receive and guess the secret offline. Tools such as hashcat (mode 16500) do this quickly. No one at your server ever sees the attempts.

Once they know the secret, they can mint a token that says they are the admin.

Fix: use a long random value, not a word. The JWT algorithm spec (RFC 7518, section 3.2) says an HS256 key must be at least 256 bits (32 bytes). Generate one:

openssl rand -base64 32

The rule for a strong JWT secret is simple: make it random, make it long, and never reuse it between apps or environments.

2. A leaked secret

A strong secret does not help if it is public. Common leaks include:

  • A .env file or config committed to a Git repository.
  • The secret hard-coded in frontend JavaScript or a mobile app.
  • A .env file reachable from the web, such as yoursite.com/.env.
  • Secrets pasted into support tickets, chat or screenshots.

Fix:

  1. Load the secret from an environment variable or a secrets manager, never from source code.
  2. Search your Git history: git log -p -S "JWT_SECRET" --all. A tool like gitleaks detect scans for many secret types.
  3. If it ever leaked, rotate it now. Changing the secret logs everyone out, which is the right outcome.
  4. Test that yoursite.com/.env and yoursite.com/.git/config both return a 404 or 403 (never 200) and never show file contents. A single-page app may return 200 with its index page for unknown paths, so check that the response is not the real file contents.

3. The none algorithm and unpinned algorithms

The JWT header says which algorithm signed the token. The spec includes a value called none, meaning "no signature." If your server reads the algorithm from the token and obeys it, an attacker can send a header like {"alg":"none"}, change the payload and leave the signature empty. Some older libraries accepted this.

A related trick is algorithm confusion. If your server uses RS256 (a public and private key pair), an attacker may switch the header to HS256 and sign with your public key, which is not secret. A real example is CVE-2015-9235 in an old version of the jsonwebtoken Node library.

Fix: tell your library exactly which algorithm to accept. Never let the token choose.

// Node.js (jsonwebtoken)
jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
# Python (PyJWT)
jwt.decode(token, key, algorithms=['HS256'])

Also keep your JWT library up to date. RFC 8725, the JWT best current practices document, recommends this approach.

4. Skipped signature checks

This one is easy to do by accident. Many libraries have a function that only reads a token and a different one that verifies it.

  • Node: jwt.decode() reads the payload and does not check the signature. jwt.verify() does.
  • Python: options={'verify_signature': False} turns checking off. It is handy for debugging and dangerous in production.

If a login route or API uses decode, anyone can write their own payload and your server will believe it.

Fix: search your codebase for decode(, verify_signature and ignoreExpiration. Every place that makes a trust decision must use the verifying function. Also check claims that identify the token's purpose: the issuer (iss) and audience (aud), if you use them.

5. No expiry on tokens

A token with no exp (expiration) claim works forever. If someone steals it from a log, a browser extension or a shared computer, they stay logged in as that user indefinitely. JWTs are stateless, so the server cannot easily cancel one early. That makes a short life even more important.

Fix:

  • Give access tokens a short life, such as 15 minutes.
  • Use a longer-lived refresh token, stored server-side so you can revoke it, to issue new access tokens.
  • Reject tokens without an exp claim.
jwt.sign({ sub: user.id, role: 'user' }, process.env.JWT_SECRET, {
  algorithm: 'HS256',
  expiresIn: '15m'
});

6. Storing tokens in localStorage

LocalStorage is a browser storage area that any JavaScript on your page can read. If an attacker finds a cross-site scripting (XSS) bug, meaning they can run their own script on your page, that script can copy the token and send it away. OWASP's HTML5 Security Cheat Sheet warns against keeping session identifiers there for this reason.

Fix: send the token in a cookie that JavaScript cannot read:

Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900
  • HttpOnly blocks scripts from reading the cookie.
  • Secure sends it only over HTTPS.
  • SameSite=Lax reduces cross-site request forgery (CSRF), where another site tricks the browser into acting for the user. For sensitive actions, add CSRF tokens too.

HttpOnly stops a script from copying the token and using it elsewhere, but XSS can still act as the user while the page is open, so fix XSS too. A Content-Security-Policy header helps reduce XSS, and our security headers tool shows whether you have one.

7. Sensitive data in the payload

Because the payload is only encoded, you can paste any JWT into a decoder and read it. Putting an email, phone number, password hash, internal notes or API keys inside is a data leak waiting to happen.

Fix: keep the payload small: a user ID (sub), a role and the expiry. Look up everything else on the server. If you truly need secret contents, use encrypted tokens (JWE) or opaque session IDs.

Is a JWT the right choice for your site?

JWTs suit APIs and services that talk to each other. For a typical small site with one server and one login form, plain server-side sessions are simpler. The server can end them instantly, and there is less to get wrong. The OWASP Session Management Cheat Sheet explains how server-side sessions should be handled. For JWT-specific guidance, see the OWASP JSON Web Token for Java Cheat Sheet. If JWTs are not buying you something specific, consider sessions.

Your JWT checklist

  • [ ] Secret is random, at least 32 bytes, and not in Git or frontend code.
  • [ ] yoursite.com/.env and yoursite.com/.git/config both return a 404 (or another error) and never show file contents.
  • [ ] The verify call lists the allowed algorithm explicitly.
  • [ ] No route uses decode-only logic to make trust decisions.
  • [ ] Every token has a short exp.
  • [ ] Tokens live in HttpOnly, Secure cookies, not localStorage.
  • [ ] The payload holds no personal data or secrets.
  • [ ] The JWT library is on a current version.

This checklist covers the most common JWT mistakes, but it is not the whole picture. Logic bugs, such as letting user A fetch user B's data by changing an ID, will not show up in this list. If you want a wider view of your exposure, read how to tell if your website is hackable or run the free website security check.

Key takeaways

  • JWTs are not insecure by nature. The risk comes from weak secrets, skipped checks and bad storage.
  • Use a random secret of at least 32 bytes, keep it out of code and Git, and rotate it if it leaks.
  • Pin the allowed algorithm and always use the verifying function, never decode alone.
  • Set a short expiry, and store tokens in HttpOnly, Secure cookies instead of localStorage.
  • Keep personal data out of the payload, because anyone can read it.

Want a person to check it for you?

A checklist catches the common mistakes. It will not find the odd ones that only show up when someone tries to break your login flow. Our $49 Circuit audit is a manual review of your whole site by a security engineer, with a written report that lists each issue, its severity, the evidence and the exact fix. It is a focused audit for small sites, not an enterprise pentest or a compliance certificate. See the pricing page for details, or read manual vs automated testing to see which fits you.

Want certainty, not guesswork?

A real human security engineer audits your whole site by hand and sends a full report: every issue, its severity, and the exact fix. From $49, with a 14-day money-back guarantee.

See pricing

Frequently asked questions

Is JWT secure or not?
A JWT is secure when it is signed with a strong secret, verified on every request, given a short expiry and stored safely. It is not secure by default, because the token only proves who made it, and it hides nothing. JWT problems usually come from setup mistakes, not from a flaw in the idea.
How do I secure a JWT secret key?
Generate a random key of at least 32 bytes, for example with `openssl rand -base64 32`. Store it in an environment variable or secrets manager, not in your code or Git history. Use a different key for each environment, and rotate it right away if it might have leaked.
Is it safe to store a JWT in localStorage?
It is risky. Any script running on your page can read localStorage, so a single cross-site scripting bug can expose every user's token. A cookie with the HttpOnly, Secure and SameSite flags is a safer default for browser logins.
What are the most common JWT security issues?
The usual ones are weak or leaked signing secrets, accepting the `none` algorithm or an unpinned algorithm, decoding without verifying the signature, tokens with no expiry, and tokens kept in localStorage. Putting personal data in the payload is another common one, since anyone can read it.
Should I use JWTs or server-side sessions for login?
For a typical small site with one server, server-side sessions are usually simpler and easier to revoke. JWTs make more sense when several services need to trust the same login without sharing a session store. Pick JWTs only if you have that need, and follow the checklist above if you do.

Keep reading

See what attackers see, for free

Run the free passive check on your website. No login, no impact on your site, results in seconds.

Ready for the full manual audit? See pricing

Published by Bug Circuit. Written with AI assistance and reviewed for accuracy before publishing.