惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
Kaspersky official blog
T
Threat Research - Cisco Blogs
N
News and Events Feed by Topic
Hacker News: Ask HN
Hacker News: Ask HN
Project Zero
Project Zero
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 叶小钗
Security Latest
Security Latest
Spread Privacy
Spread Privacy
aimingoo的专栏
aimingoo的专栏
N
News and Events Feed by Topic
Webroot Blog
Webroot Blog
U
Unit 42
Cyberwarzone
Cyberwarzone
小众软件
小众软件
Scott Helme
Scott Helme
Engineering at Meta
Engineering at Meta
Microsoft Security Blog
Microsoft Security Blog
T
The Blog of Author Tim Ferriss
A
About on SuperTechFans
爱范儿
爱范儿
S
Schneier on Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Schneier on Security
Schneier on Security
Latest news
Latest news
GbyAI
GbyAI
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园_首页
Blog — PlanetScale
Blog — PlanetScale
PCI Perspectives
PCI Perspectives
Jina AI
Jina AI
AI
AI
NISL@THU
NISL@THU
I
Intezer
G
GRAHAM CLULEY
B
Blog
S
Secure Thoughts
IT之家
IT之家
宝玉的分享
宝玉的分享
Recent Announcements
Recent Announcements
Y
Y Combinator Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell
有赞技术团队
有赞技术团队
V2EX - 技术
V2EX - 技术
Recorded Future
Recorded Future
Hacker News - Newest:
Hacker News - Newest: "LLM"

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
JWT in Node.js: How It Works, 5 Errors That Compromise Your API, and Refresh Token with Rotation
Dev Code Sof · 2026-05-17 · via DEV Community

A dev submitted a PR with CPF and password hash inside the JWT payload. He thought Base64 was encryption. The reviewer rejected it, opened an urgent card, and spent the afternoon explaining the problem to the team.

That specific mistake isn't rare — and it's not even the most dangerous one you'll find in JWT implementations out there.

If you use JWT in production or are about to, this post covers what really matters: the structure, the correct flow, the errors that compromise everything, and how to implement refresh token rotation for real.


JWT vs Session: the choice that defines your architecture

Before any code, the right question is: do you actually need JWT or server-side sessions?

Criteria JWT (stateless) Session + Redis (stateful)
Immediate revocation ❌ Not native ✓ Simple
Horizontal scalability ✓ No shared state ⚠️ Requires shared Redis
Microservices / multiple services ✓ Token carries context ❌ Session must be accessible
Instant logout ❌ Only with blocklist ✓ Destroys the session
Implementation complexity Medium Low

JWT works well for service-to-service communication, mobile-consumed APIs, and architectures where you don't want to depend on shared state. Session with Redis is simpler when you have a traditional web app that needs granular control — logout across all devices, immediate bans, permission changes that take effect on the next request.

Choosing JWT because "it's modern" without considering revocation needs is the start of many problems.


The real token structure

JWT is not encryption. It's a signing mechanism. The content is Base64URL-encoded — anyone with the token can read the payload. Security comes from signature integrity, not data secrecy.

A token has three parts separated by dots:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzAxIn0.SflKxwRJSMeKKF2Qt4fwpM
       HEADER                   PAYLOAD                  SIGNATURE

Enter fullscreen mode Exit fullscreen mode

Header — declares the signing algorithm:

{
  "alg": "HS256",
  "typ": "JWT"
}

Enter fullscreen mode Exit fullscreen mode

Payload — the claims: data you want to transmit:

{
  "sub": "user_01HXYZ",
  "role": "admin",
  "plan": "pro",
  "iat": 1747267200,
  "exp": 1747270800
}

Enter fullscreen mode Exit fullscreen mode

Signature — what guarantees nothing was altered:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  JWT_SECRET
)

Enter fullscreen mode Exit fullscreen mode

💡 Want to inspect a token in real time? Use the JWT Decoder — paste any token and see the decoded header, payload, and expiration instantly, no install needed.

iat (issued at), exp (expiration), sub (subject), and jti (JWT ID) are registered claims from RFC 7519. Use them instead of creating custom fields with the same purpose — every JWT library knows how to interpret them.


The complete flow with code

The basic authentication cycle:

[client]  POST /auth/login  →  [server validates credentials]
                            ←  access_token (15min) + refresh_token (7d)
[client]  GET /api/data     →  Authorization: Bearer <access_token>
                            ←  200 OK (server verified signature locally)
[expired] POST /auth/refresh →  refresh_token in body or cookie
                            ←  new access_token + new refresh_token

Enter fullscreen mode Exit fullscreen mode

Authentication middleware in TypeScript:

import jwt from "jsonwebtoken";
import type { Request, Response, NextFunction } from "express";

function generateAccessToken(userId: string, role: string): string {
  return jwt.sign(
    { sub: userId, role },
    process.env.JWT_SECRET!,
    { expiresIn: "15m", algorithm: "HS256" }
  );
}

function authenticate(req: Request, res: Response, next: NextFunction) {
  const auth = req.headers.authorization;

  if (!auth?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Token not provided" });
  }

  try {
    const token = auth.split(" ")[1];
    const payload = jwt.verify(token, process.env.JWT_SECRET!, {
      algorithms: ["HS256"],
    });
    req.user = payload as TokenPayload;
    next();
  } catch {
    return res.status(401).json({ error: "Invalid or expired token" });
  }
}

Enter fullscreen mode Exit fullscreen mode

Note the algorithms: ["HS256"] in the verification. That's not a detail — it's what prevents a real attack vector.


5 errors that compromise your API

1. Sensitive data in the payload

// ❌ Wrong
const token = jwt.sign(
  {
    sub: user.id,
    password_hash: user.password,
    ssn: user.ssn,
    credit_card: user.cardNumber,
  },
  SECRET
);

Enter fullscreen mode Exit fullscreen mode

Anyone with that token reads all of it. No cracking needed. Base64 decodes in milliseconds. The payload should carry only what's necessary for authorization — sub, role, plan. If you need more user data, query the database using sub.

2. Accepting the none algorithm

The most well-known vulnerability in the JWT ecosystem: some older libraries accept alg: "none" in the header, eliminating signature verification entirely. An attacker edits the payload, declares none, and the server accepts it.

// ❌ Vulnerable
jwt.verify(token, secret);

// ✅ Safe
jwt.verify(token, secret, { algorithms: ["HS256"] });

Enter fullscreen mode Exit fullscreen mode

3. Weak secret or secret committed to the repository

# ❌ All of these are dangerous
JWT_SECRET=secret
JWT_SECRET=my-api-2024
JWT_SECRET=abc123

Enter fullscreen mode Exit fullscreen mode

A short secret can be broken by offline brute force. A secret in the repository means anyone with access to the Git history has permanent access, even after rotation.

# ✅ Generate a proper secret
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

Enter fullscreen mode Exit fullscreen mode

Use a secrets service in production: AWS Secrets Manager, HashiCorp Vault, Doppler.

4. Token without expiration

// ❌ Valid forever
jwt.sign({ sub: userId }, SECRET);

// ✅ Short-lived
jwt.sign({ sub: userId }, SECRET, { expiresIn: "15m" });

Enter fullscreen mode Exit fullscreen mode

A token without exp is valid forever. If it appears in an error log, a debug response, or a Slack screenshot — it works tomorrow, in six months, two years from now. Short expiration limits the damage window of any leak.

5. Not validating aud and iss in multi-service systems

If you have more than one service issuing or consuming JWTs, a token generated for service A may be accepted by service B if neither validates aud (audience) and iss (issuer).

// Signing
jwt.sign(
  { sub: userId, role: "user" },
  SECRET,
  { expiresIn: "15m", issuer: "auth-service", audience: "api-service" }
);

// Verifying
jwt.verify(token, SECRET, {
  algorithms: ["HS256"],
  issuer: "auth-service",
  audience: "api-service",
});

Enter fullscreen mode Exit fullscreen mode

⚠️ Important: JWT has no native revocation. A valid token issued at 2pm is still valid at 2:14pm even if the user changed their password at 2:01pm. If your system needs immediate invalidation, you'll need a blocklist — which breaks part of the stateless advantage. Consider server-side sessions for those cases.


Secure storage: localStorage or HttpOnly cookie?

Where the client stores the token defines which attack surface you need to mitigate.

Strategy XSS CSRF Complexity
localStorage Vulnerable Immune Low
HttpOnly Cookie Immune Vulnerable Medium
HttpOnly + SameSite=Strict + CSRF token Immune Immune High
HttpOnly + SameSite=Lax (modern default) Immune Protected for most cases Medium

Cookie with HttpOnly is the safer option because JavaScript can't access the value. Full configuration:

res.cookie("access_token", token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: "strict",
  maxAge: 15 * 60 * 1000,
  path: "/",
});

Enter fullscreen mode Exit fullscreen mode

There's no universally correct choice. Assess your real attack surface: if you have a lot of third-party content on the page, HttpOnly protects more. If your API is consumed by many different clients, localStorage may be more practical with proper XSS mitigations.


Refresh token with secure rotation

A 15-minute access token is secure but unworkable without a refresh token. The rotation flow:

async function refreshAccessToken(refreshToken: string) {
  const stored = await db.refreshToken.findUnique({
    where: { token: refreshToken },
    include: { user: true },
  });

  if (!stored || stored.expiresAt < new Date() || stored.revoked) {
    throw new Error("Invalid or revoked refresh token");
  }

  // Revoke the used token
  await db.refreshToken.update({
    where: { id: stored.id },
    data: { revoked: true },
  });

  // Issue a new one in the same family
  const newRefresh = await db.refreshToken.create({
    data: {
      token: crypto.randomUUID(),
      userId: stored.userId,
      familyId: stored.familyId, // 👈 key for detecting reuse
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });

  const accessToken = generateAccessToken(stored.userId, stored.user.role);

  return { accessToken, refreshToken: newRefresh.token };
}

Enter fullscreen mode Exit fullscreen mode

The familyId field is what allows detecting reuse. When a refresh token is used, it's revoked and a new one is issued in the same family. If the revoked token appears again (which only happens if there was a leak), you identify the family and immediately revoke all tokens for that user.

💡 Refresh tokens are always stateful — they need to exist in the database. It's precisely because they allow issuing new access tokens that they need to be tracked and revocable.


Security checklist before going to production

Run this before any deploy:

  • [ ] JWT_SECRET is at least 64 randomly generated bytes
  • [ ] JWT_SECRET is not in the repository (not even in Git history)
  • [ ] Access token has short expiration (15m to 1h)
  • [ ] algorithms is fixed in verification (["HS256"] or ["RS256"])
  • [ ] Payload contains no sensitive data beyond what's needed for authorization
  • [ ] Refresh tokens are stored in the database with a revoked field
  • [ ] Refresh token rotation is implemented with reuse detection
  • [ ] Logout revokes the refresh token in the database
  • [ ] User tokens are all revoked on password change or compromised account
  • [ ] In multi-service systems, iss and aud are validated
  • [ ] Cookie configured with HttpOnly, Secure, and SameSite appropriate for context
  • [ ] Error logs don't expose token values

FAQ

JWT or session for a traditional web app?
If you need immediate revocation (logout, bans, permission changes), session with Redis is simpler. JWT is better when you have multiple services or mobile clients. For traditional monoliths, sessions usually solve the problem with less complexity.

HS256 or RS256?
HS256 uses a symmetric key — the same secret signs and verifies. RS256 uses an asymmetric pair — private key signs, public key verifies. If only one service issues tokens, HS256 is sufficient. If multiple services need to verify without issuing, distribute the public key and use RS256.

What to do if JWT_SECRET leaks?
Rotate the secret immediately — this invalidates all existing tokens. Investigate the source of the leak before generating the new one. If you're on AWS, use Secrets Manager for automatic rotation and access auditing.

Can I invalidate a JWT before it expires?
Not natively. You need a blocklist: store the jti of revoked tokens in Redis with TTL equal to the token's exp. Every verification queries Redis. Simple, but adds a network dependency per request.


Next steps

  • Run the checklist now on any JWT project you have in production — especially the algorithm and payload items
  • Implement familyId on refresh tokens if you haven't yet — that's what separates a functional implementation from a secure one
  • Explore JWKS if you work with multiple services — the auth server exposes public keys via endpoint and each service fetches them dynamically
  • Read the OWASP JWT Security Cheat Sheet — covers vectors like kid injection and jku spoofing that are real in more complex environments

Originally published on Dev Code Software.