Skip to content
KhaiziNam Blog KhaiziNam Blog
Go back
Đọc bằng tiếng Việt

JWT Security Best Practices: How to Secure JSON Web Tokens in Production 2026

Summary of 10+ of the most important JWT security best practices in 2026 — from secure token storage and choosing the right signing algorithm to handling revocation — helping developers avoid critical security vulnerabilities in production projects.

You already know what JWT is and have used it in projects, but are you making security mistakes that hackers could exploit in minutes? Many developers implement JWT correctly in terms of functionality but completely wrong in terms of security — storing tokens in localStorage, using weak secret keys, failing to rotate refresh tokens, or ignoring claim validation. A system compromised due to JWT misconfiguration can lose all user sessions in just a few hours. This article goes straight into each specific best practice, explaining why and guiding you on how to implement it correctly.

Article Content:


1. The Nature of JWT Security Risks

1.1 Why is JWT easier to misconfigure than Sessions?

JWT is a self-contained technology — all authentication information resides within the token, and the server does not store state. This creates high flexibility but also shifts security responsibility entirely to the developer. With Sessions, the server has direct control: to kick a user out, just delete the session. With JWT, if a token is leaked and you don’t have a revocation mechanism, an attacker can use that token for its entire remaining duration — which could be 24 hours, or even 7 days if configured incorrectly.

1.2 Practical JWT attack vectors you need to know

2. Before/After applying JWT Security Best Practices

2.1 System before applying practices
2.2 System after applying best practices

3. Choosing the right signing algorithm: HS256, RS256, ES256

3.1 HS256 — When is it suitable?

HS256 (HMAC SHA-256) is a symmetric algorithm: the same secret key is used for both signing and verification. Suitable when there is only one service that both issues and authenticates tokens. Pros: simple, fast. Cons: if multiple services need verification, all must hold the secret — leaking risk increases with the number of services.

// Node.js — Generating a strong secret key
const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex');
// Result: 64 hex characters = 256 bit entropy

// Signing the token
const jwt = require('jsonwebtoken');
const token = jwt.sign(
  { sub: userId, role: 'user', iss: 'khaizinam.io.vn', aud: 'app' },
  secret,
  { algorithm: 'HS256', expiresIn: '15m' }
);
3.2 RS256 — The choice for microservices

RS256 is asymmetric: a private key for signing (held only by the Auth Service), and a public key for verification (can be made public via a JWKS endpoint). This is the best practice for microservices systems or when third parties need to verify tokens.

// Node.js — Signing with RS256
const fs = require('fs');
const privateKey = fs.readFileSync('private.pem');

const token = jwt.sign(
  { sub: userId, iss: 'auth.khaizinam.io.vn', aud: 'api' },
  privateKey,
  { algorithm: 'RS256', expiresIn: '15m' }
);

// Verifying with public key
const publicKey = fs.readFileSync('public.pem');
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // ALWAYS whitelist the algorithm
  issuer: 'auth.khaizinam.io.vn',
  audience: 'api'
});
3.3 ES256 — When higher performance than RS256 is needed

ES256 (ECDSA with P-256) is also asymmetric but with smaller keys and significantly faster than RS256 — suitable for mobile or IoT. It offers equivalent security to RS256 but the signature is ~3 times smaller, reducing network overhead.

4. Secure Token Storage — Decisions affecting overall security

4.1 Why is localStorage the wrong choice?

localStorage is accessible by any JavaScript running on the same origin — including scripts injected via XSS. A minor XSS vulnerability (e.g., rendering un-sanitized user input) is enough for an attacker to run localStorage.getItem(‘token’) and send it to their server. This isn’t theory — this is an attack vector exploited in many real-world bug bounty reports.

// Node.js / Express — Setting refresh token in httpOnly cookie
res.cookie('refreshToken', refreshToken, {
  httpOnly: true,      // JavaScript cannot read it
  secure: true,        // Only send via HTTPS
  sameSite: 'strict',  // Block CSRF
  maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
});

// Access token: sent in response body, client stores in memory (JS variable)
// Do not store in localStorage or sessionStorage
res.json({ accessToken });
4.3 Storage strategy by platform

5. Validating Claims & Expiry correctly

5.1 Mandatory claims to validate

Many developers only verify the signature and ignore claim validation — this is a serious vulnerability. You must validate:

// Laravel PHP — Full validation with firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$decoded = JWT::decode($token, new Key($publicKey, 'RS256'));

// Manual additional validation
if ($decoded->iss !== 'auth.khaizinam.io.vn') {
    throw new \Exception('Invalid issuer');
}
if ($decoded->aud !== 'api') {
    throw new \Exception('Invalid audience');
}
// exp is automatically checked by firebase/php-jwt — throws ExpiredException if expired
5.2 Clock Skew — The issue few notice

In distributed environments, clocks between servers can drift by a few seconds. A newly created token might be rejected by another service for “not yet valid (nbf)” or “expired (exp)”. Best practice: allow a clock skew of up to 30–60 seconds during validation.

6. Handling Revocation & Refresh Token Rotation

6.1 Core problem: JWT cannot revoke itself

This is a design trade-off of JWT compared to Sessions. Once issued, a token is valid until it expires — even if the user changes their password, is banned, or an admin wants to kick them. Solutions must be built externally:

6.2 Jti Blacklist with Redis — Instant Revocation
// Upon logout or when immediate revocation is needed
// 1. Read jti from the current token
const decoded = jwt.decode(token);
const jti = decoded.jti;
const ttl = decoded.exp - Math.floor(Date.now() / 1000);

// 2. Store in Redis with TTL = remaining time of the token
await redis.setex(`blacklist:${jti}`, ttl, '1');

// 3. Verify middleware — check blacklist before allowing access
const isBlacklisted = await redis.get(`blacklist:${jti}`);
if (isBlacklisted) {
    return res.status(401).json({ error: 'Token revoked' });
}
6.3 Refresh Token Rotation — Detecting Token Theft

Rotation is a mechanism where every time a refresh token is used to get a new access token, the old refresh token is invalidated and a new one is issued. If an attacker steals a refresh token and uses it before the user, when the real user attempts to use it, the system sees the token is already invalid — immediately indicating a breach and allowing for the revocation of the entire session.

// Node.js — Refresh Token Rotation logic
async function refreshTokens(oldRefreshToken) {
  // 1. Find in DB
  const storedToken = await db.refreshTokens.findOne({
    token: hash(oldRefreshToken),
    revoked: false
  });

  if (!storedToken) {
    // Token doesn't exist or is already revoked — possible reuse attack
    // Revoke all sessions for this user
    await db.refreshTokens.updateMany(
      { userId: storedToken?.userId },
      { revoked: true }
    );
    throw new Error('Refresh token reuse detected');
  }

  // 2. Revoke old token
  await db.refreshTokens.updateOne(
    { _id: storedToken._id },
    { revoked: true }
  );

  // 3. Issue new tokens
  const newAccessToken = jwt.sign({ sub: storedToken.userId }, secret, { expiresIn: '15m' });
  const newRefreshToken = crypto.randomBytes(40).toString('hex');

  await db.refreshTokens.create({
    token: hash(newRefreshToken),
    userId: storedToken.userId,
    expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
  });
}

7. 6 common JWT security mistakes and how to fix them

  1. Using weak secrets or hardcoding them → Fix: Generate using crypto.randomBytes(32), store in environment variables, and never commit to Git. Rotate keys periodically.
  2. Not whitelisting algorithms → Fix: Always pass algorithms: [‘RS256’] when verifying. Attackers won’t be able to switch to alg:none or bypass security using public keys.
  3. Storing access tokens in localStorage → Fix: Access token in-memory, refresh token in httpOnly cookie. Accept the UX trade-off for security.
  4. TTLs that are too long (24h, 7 days) for access tokens → Fix: Access token 15 minutes, refresh token 7–30 days. Use silent refresh to maintain UX.
  5. Not validating iss and aud → Fix: Always set issuer and audience when signing, and verify when decoding. Crucial in multi-service architectures.
  6. Storing sensitive information in payload → Fix: Payload should only contain userId, role, exp — no emails, passwords, or PII. The payload is only encoded, not encrypted.

To understand more about how JWT works in technical interviews, see Common JWT Interview Questions and Standard Answers 2026. If you’re unsure whether to choose JWT or Session, Session vs JWT: Full Theory and Interview Questions will help you decide.

8. FAQ - Frequently Asked Questions about JWT Security

8.1 Does JWT need HTTPS?

Absolutely. JWT is signed, not encrypted — anyone intercepting HTTP traffic can read and steal the token. HTTPS is the minimum requirement for JWT security to be meaningful.

8.2 Should I use JWE instead of JWT?

JWE (JSON Web Encryption) encrypts the payload — suitable if the payload contains sensitive info that must be kept secret from third parties. However, JWE is significantly more complex and has higher CPU overhead. Practical best practice: just don’t store sensitive info in the payload.

8.3 Should I use JWT for regular web sessions?

Not necessarily. For traditional web apps (server-side rendered, no immediate need for horizontal scaling), Session + cookies are simpler, offer built-in revocation, and have fewer pitfalls. JWT shines in REST APIs, microservices, and mobile.

8.4 How to rotate secret keys while users are logged in?

Use key versioning: add a kid (key ID) claim to the header; the server keeps multiple keys mapped by kid. When rotating, issue new tokens with the new kid — old tokens remain valid until they expire. This allows zero-downtime key rotation.

8.5 Is JWT suitable for real-time apps (WebSockets)?

Yes, but it requires manual handling. WebSockets do not have automatic Authorization headers like HTTP. Common method: send the token in the first message after connection; the server verifies it before processing any further messages.

Summary & Next Steps

JWT security is not a one-time checklist — it’s a design mindset. Remember the three pillars: choose the right algorithm (RS256 for multi-service), store tokens correctly (httpOnly cookies for refresh), and always have a revocation mechanism. Review your current codebase with this checklist — chances are you’ll find at least one point for improvement.

Author: Nguyen Huu Khai

21/04/2026

See more: 


Share this post:

Related Posts

IT Fresher & Junior Salary 2026: PHP, Node.js, React, Flutter - Real Market Data

Real salary benchmarks for IT freshers and junior developers in Vietnam in 2026, broken down by tech stack - so you know exactly what number to quote in interviews, or whether you're being underpaid right now.

Junior Developer Portfolio: What to Include to Get Interview Calls

A junior developer portfolio is the collection of real projects, skills, and professional information you present so employers can evaluate your abilities — substituting for the work experience section of your CV that's currently empty. For freshers and junior developers, a portfolio isn't something that's "nice to have" — it's the only evidence you can offer to prove you can actually build things

How to Write a Junior Developer Cover Letter With No Experience

Learn how to write a junior developer cover letter with no experience, including practical templates, real examples, common mistakes, and tips to pass the CV screening round.

Body Language in Tech Interviews: 7 Mistakes That Cost You the Offer

Body language in tech interviews refers to the non-verbal signals — posture, eye contact, hand gestures, and vocal tone — that recruiters observe alongside your technical answers. Mastering these signals helps you project confidence and professionalism from the very first second you walk into the room.

Questions to Ask Your Interviewer as a Junior Dev (Beyond Just Salary)

A practical guide to asking questions in IT job interviews — why the "do you have any questions?" moment matters more than most candidates realize, 20+ real questions organized by purpose and interview round, what never to ask, and how to choose the right questions for each interviewer you face.

Why Did You Choose IT? An Interview Answer That Actually Impresses Recruiters

A practical guide to answering "Why did you choose IT?" in job interviews — what HR is actually evaluating, a 3-part framework for building your answer, script templates for freshers and career changers, and the common mistakes that make this answer sound hollow and unconvincing.

'What Is Your Greatest Weakness?' — IT Interview Answer That Won't Get You Rejected

A practical guide to answering "What is your greatest weakness?" in IT job interviews — why the question is a trap for most candidates, a 3-step framework for freshers and junior developers, ready-to-use script templates, real case studies, and the six most common mistakes that get candidates rejected on the spot.

ReactJS Junior Interview Questions: 30+ Real Questions With Answers 2026

30+ of the most commonly asked ReactJS junior interview questions in 2026 — covering Virtual DOM, hooks, state management, and performance optimization — with detailed answers and code examples to help you confidently pass any technical interview. You've been learning React for a few months, you've built projects, but every time you walk into an interview you get asked things no tutorial ever cove

Refresh Token in Node.js and Laravel: Complete Production Implementation Guide 2026

A complete guide to implementing Refresh Tokens in both Node.js (Express) and Laravel PHP — covering database design, API endpoints, rotation logic, and common pitfalls — helping developers build a production-ready JWT authentication system in 2026.

Session vs JWT: Which Should Developers Choose? A Practical Comparison for 2026

A detailed comparison of Session and JWT across 6 practical technical criteria — architecture, revocation, scaling, performance, security, and complexity — helping developers make the right choice for each type of project in 2026.


Previous Post
Session vs JWT: Which Should Developers Choose? A Practical Comparison for 2026
Next Post
JWT Interview Questions and Answers (2026)