Development

JWT Security Best Practices (Avoid the Common Attacks)

By AZ Utils Editorial · · 12 min read

JWT Security Best Practices (Avoid the Common Attacks)

JWTs are powerful, but they are also easy to get wrong — and the failures tend to be serious, because a broken token check can let an attacker impersonate any user. Most JWT vulnerabilities are not exotic; they come from skipping a verification step or trusting something you should not. This guide collects the JWT security best practices that actually matter, explains the attacks they prevent, and shows the code that gets it right.

It is written for developers and engineers responsible for authentication, and for anyone reviewing a JWT implementation for safety.

The Foundation: Always Verify, Never Just Decode

The most important habit in JWT security is to understand that decoding and verifying are different operations. Decoding reads the Base64url header and payload — anyone can do it, and it proves nothing. Verifying recomputes the signature with your key and confirms the token is authentic and unaltered. Never trust a claim from a token you have not verified.

This sounds obvious, yet a surprising number of bugs come from code that decodes a token to read, say, the user ID, and acts on it without checking the signature. An attacker can craft any payload they like; only verification stops them. Treat every incoming token as hostile until the signature check passes.

In short: Verify every token's signature with a pinned algorithm, validate the claims (exp, aud, iss), keep tokens short-lived with a revocation strategy, store and transmit them safely, and never put secrets in the payload.

Pin the Algorithm: Defeating "alg: none" and Confusion Attacks

A JWT's header declares the signing algorithm in its alg field. The danger is that this field is attacker-controlled, and naive verification trusts it. Two classic attacks exploit this:

  • The alg: none attack. The JWT spec defines an "unsecured" token with alg: none and no signature. If your verifier accepts it, an attacker can forge any payload with no signature at all. Always reject none.
  • Algorithm confusion (RS256 → HS256). If your system uses RSA (RS256), it verifies with a public key, which is not secret. An attacker can change the header to HS256 and sign the token using your public key as the HMAC secret. A verifier that trusts the header's algorithm will validate the forgery.

The defence is the same in both cases: pin the expected algorithm in your verification call and never let the token choose it.

// Correct: the server dictates the algorithm
jwt.verify(token, key, { algorithms: ["RS256"] });

// Dangerous: trusting whatever the token's header claims
jwt.verify(token, key); // do NOT rely on header-driven alg

Validate the Claims, Not Just the Signature

A valid signature only tells you the token is authentic — not that it is appropriate for this request. You must also validate the claims:

  • exp (expiry). Reject expired tokens. This is the single most important claim check.
  • aud (audience). Confirm the token was meant for your service, so a token minted for another service cannot be replayed against yours.
  • iss (issuer). Confirm it came from the issuer you trust.
  • nbf (not before). Reject tokens used before their activation time.

Good libraries check these automatically when you supply the expected issuer, audience and a clock tolerance — covered in detail in Common JWT Claims. The mistake to avoid is verifying the signature and then forgetting the claims entirely.

Keep Tokens Short-Lived and Plan for Revocation

The Achilles' heel of a self-contained JWT is that it cannot be easily revoked — once issued, it is valid until it expires. If a long-lived token is stolen, the attacker has a long window to abuse it. The standard mitigations:

  • Short-lived access tokens. Minutes, not days. A stolen token expires quickly.
  • Refresh tokens for longevity. A separate, securely stored, revocable refresh token issues new access tokens, so users stay logged in without long-lived access tokens.
  • A revocation mechanism. Maintain a denylist of revoked token IDs (jti), or rotate refresh tokens on each use so a stolen one is detected.

This combination restores the ability to actually log a user out — the main thing pure stateless tokens give up.

Store and Transmit Tokens Safely

Because a Bearer token is the credential, whoever holds it is the user. Protecting it is as important as the signature:

  • HTTPS always. Never send tokens over plain HTTP, where they can be intercepted.
  • Never in URLs. Tokens in query strings leak into server logs, browser history, proxies and Referer headers. Use headers or cookies.
  • Mind XSS vs CSRF. Storing a token in localStorage exposes it to theft via cross-site scripting (XSS). Storing it in an httpOnly cookie hides it from JavaScript (mitigating XSS theft) but introduces cross-site request forgery (CSRF) concerns, which you counter with SameSite cookie settings and CSRF tokens. Choose deliberately based on your threat model.

Use Strong Keys and Rotate Them

The security of an HMAC-signed (HS256) token rests entirely on the secret. A weak or guessable secret can be brute-forced offline, letting an attacker forge tokens. Use a long, random, high-entropy secret, store it securely (a secrets manager, not source code), and rotate it periodically. For asymmetric algorithms (RS256, ES256), protect the private key rigorously and publish only the public key, typically via a JWKS endpoint so verifiers can fetch and rotate keys cleanly.

Never Put Secrets in the Payload

This bears repeating because it is so commonly violated: the payload is only Base64url-encoded and readable by anyone who holds the token. Do not put passwords, full personal records, internal secrets or anything sensitive in a claim. If data genuinely must be confidential, encrypt it (for example with JWE) or keep it server-side and reference it by an opaque ID. You can confirm exactly what a token exposes by decoding it in our JWT Decoder.

Why JWT Mistakes Are So Costly

It is worth understanding why JWT bugs deserve special care, because the stakes are higher than they first appear. A JWT is, in effect, a portable proof of identity. If an attacker can forge one — by exploiting alg: none, an algorithm-confusion flaw, or a weak secret — they do not gain access to a single account; they gain the ability to mint a token for any user, including administrators. A single verification weakness can therefore escalate into a full authentication bypass across the entire system. Unlike a leaked password, which compromises one account, a broken token check compromises the mechanism that protects all of them.

The same amplification applies to stolen tokens. Because a JWT is a bearer credential and is hard to revoke, a token captured from an insecure channel or script-accessible storage can be replayed for its entire lifetime. This is why the seemingly small details — pinning the algorithm, keeping lifetimes short, transmitting only over HTTPS — are not pedantic box-ticking but the actual load-bearing walls of the system. The cost of getting them wrong is measured in total compromise, not inconvenience.

A Threat-Model Walkthrough

A useful way to sanity-check an implementation is to reason through the attacks it must withstand and confirm which control stops each one:

  • Forged token (attacker writes their own payload). Stopped by signature verification with a pinned algorithm. Without pinning, alg: none or algorithm confusion defeats this, which is why pinning is non-negotiable.
  • Tampered token (attacker edits a real token). Stopped by the signature check — any change to header or payload invalidates it.
  • Replayed token from another service. Stopped by validating the aud claim, so a token minted for one audience is rejected elsewhere.
  • Expired or stale token. Stopped by validating exp (and keeping lifetimes short).
  • Stolen token (intercepted or exfiltrated). Mitigated by HTTPS, careful storage, short lifetimes and a revocation path; you cannot prevent theft entirely, so you limit the damage window.
  • Brute-forced secret. Stopped by using a long, high-entropy secret or proper key pair.

If you can point to the specific control that defeats each row, your implementation is probably sound. If any row has no answer, you have found your next task.

Defense in Depth Beyond the Token

Finally, remember that token hygiene is necessary but not sufficient; it sits inside a wider security posture. Rate limiting protects your login and refresh endpoints from brute-force and credential-stuffing attempts. Strong password hashing protects the credentials that mint tokens in the first place. Monitoring and logging — with sensitive values scrubbed — let you detect anomalous token use, such as the same token appearing from wildly different locations. And the principle of least privilege keeps the blast radius small: a token that grants only the scopes a client genuinely needs is far less dangerous if it leaks than one that grants broad access. JWTs are one layer in this stack, and they work best when the layers around them are healthy too.

Try Our Free JWT Decoder

A quick way to audit what your tokens reveal and whether their claims look right is our JWT Decoder.

  • ✅ See exactly what is in the header and payload
  • ✅ Spot sensitive data that should not be there
  • ✅ Runs in your browser — the token is never uploaded

👉 Audit a token now →

Putting the Practices Together

Individually, each of these practices closes one gap; together they form a coherent posture that is far stronger than any single control. The mental model that ties them together is simple: a token must be authentic, appropriate, and contained. Authentic means the signature verifies under an algorithm you chose, not one the attacker chose — this is the line that stops forgeries. Appropriate means the claims fit the request: it has not expired, it was issued by your trusted issuer, and it names your service as its audience — this is the line that stops valid-but-misused tokens. Contained means that even when something goes wrong, the damage is bounded — short lifetimes shrink the window of a stolen token, careful storage and HTTPS reduce the chance of theft, a revocation path lets you cut a session off, and least-privilege scopes limit what a leaked token can do.

When you review an implementation, walking through those three properties in order is a fast way to find weaknesses. Most real-world JWT breaches trace back to a single missing line in one of them: a verifier that did not pin the algorithm, a service that checked the signature but not the expiry, or a token stored somewhere a script could read it. None of these are sophisticated attacks; they are omissions. The reassuring flip side is that a team which consistently applies the full set — verify with a pinned algorithm, validate the claims, keep tokens short-lived and revocable, transmit and store them carefully, and protect the keys — closes off essentially the entire catalogue of common JWT vulnerabilities. Security here is less about cleverness than about not skipping steps, and building the habit of applying all of them every time.

Common Mistakes

  1. Accepting alg: none or trusting the header's algorithm. Always pin the algorithm.
  2. Decoding without verifying. Reading claims is not trusting them.
  3. Skipping claim validation. A valid signature is not enough — check exp, aud and iss.
  4. Long-lived tokens with no revocation. A stolen token stays valid until expiry.
  5. Weak HMAC secrets. Guessable secrets can be brute-forced to forge tokens.
  6. Putting secrets in the payload or sending tokens over HTTP or in URLs.

Best Practices Checklist

  • Verify every token with a trusted library and a pinned algorithm.
  • Validate exp, and validate aud and iss against expected values, with a little clock leeway.
  • Use short-lived access tokens plus revocable refresh tokens.
  • Maintain a revocation path (denylist by jti or refresh-token rotation).
  • Transmit only over HTTPS; never put tokens in URLs.
  • Choose storage (httpOnly cookie vs browser storage) based on your XSS/CSRF threat model.
  • Use strong, rotated keys; protect private keys; publish public keys via JWKS.
  • Keep payloads minimal and non-secret.

Frequently Asked Questions

What is the most important JWT security rule?

Always verify the token's signature with a pinned algorithm before trusting any claim. Decoding a token is not the same as verifying it, and an unverified token can be forged.

What is the alg:none attack?

It exploits the JWT "unsecured" mode where alg is set to none and there is no signature. If a verifier accepts it, an attacker can forge any payload without signing it. Always reject the none algorithm and pin the expected one.

Why should I pin the JWT algorithm?

Because the algorithm field is attacker-controlled. Pinning it prevents algorithm-confusion attacks, such as switching an RS256 token to HS256 and signing it with the public key, which a header-trusting verifier would accept.

How do I revoke a JWT?

Because a self-contained JWT cannot be deleted, use short-lived access tokens with revocable refresh tokens, or maintain a server-side denylist of revoked token IDs (jti). This restores the ability to log users out.

Where should I store a JWT on the client?

An httpOnly cookie hides the token from JavaScript, mitigating theft via XSS but requiring CSRF protections (SameSite, CSRF tokens). Browser storage is simpler but exposed to XSS. Choose based on your threat model.

Can I put user data in a JWT?

You can include non-sensitive identity and authorization data, like a user ID and role, but never secrets or sensitive personal data, because the payload is readable by anyone. Encrypt or keep such data server-side.

Summary

JWT security is mostly about discipline rather than cleverness. Verify every token with a pinned algorithm so forgeries and alg: none attacks fail. Validate the claims — especially exp, aud and iss — so a valid signature alone is not enough. Keep access tokens short-lived, back them with revocable refresh tokens, and maintain a revocation path so you can truly log users out. Protect the token in transit and at rest, use strong rotated keys, and never let the payload hold a secret. Follow this checklist and the common JWT vulnerabilities simply do not apply to your system. When in doubt about what a token contains, decode it with the free tool below and check.

One last piece of practical advice: make these checks the default in your codebase rather than something each developer remembers to add. Wrap token verification in a single, well-reviewed function or middleware that pins the algorithm, validates the issuer, audience and expiry, and applies a sensible clock tolerance — then require every protected route to go through it. Centralising the logic means the secure path is the easy path, and a future change (rotating a key, tightening a lifetime, adding a denylist check) happens in one place instead of being scattered and inconsistent. Security that depends on everyone remembering to do the right thing eventually fails; security baked into shared, mandatory infrastructure holds up. Treat your verification routine as that infrastructure, review it carefully, and let it quietly enforce the practices in this guide on every request.

👉 Check your tokens with our free JWT Decoder →

AZ Utils Editorial

AZ Utils Editorial

Finance & web-tools writer

AZ Utilis writes practical, plain-English guides on calculators, finance and everyday web tools, drawing on years of experience helping beginners and small businesses get the numbers right.

Development

How to Format JSON (Beautify & Minify)

How to format JSON — beautify it for readability or minify it for production — in tools, editors, the command line and code, with the why behind each.

AZ Utils Editorial · · 10 min read