[{"data":1,"prerenderedAt":338},["ShallowReactive",2],{"blog-how-to/jwt-security":3},{"id":4,"title":5,"body":6,"category":319,"date":320,"dateModified":320,"description":321,"draft":322,"extension":323,"faq":324,"featured":322,"headerVariant":325,"image":324,"keywords":324,"meta":326,"navigation":327,"ogDescription":328,"ogTitle":324,"path":329,"readTime":324,"schemaOrg":330,"schemaType":331,"seo":332,"sitemap":333,"stem":334,"tags":335,"twitterCard":336,"__hash__":337},"blog/blog/how-to/jwt-security.md","How to Implement JWT Security",{"type":7,"value":8,"toc":304},"minimark",[9,13,17,21,27,30,43,48,51,55,75,88,101,114,127,140,180,184,211,215,220,223,227,230,234,237,241,244,266,285],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-implement-jwt-security",[18,19,20],"p",{},"Build secure token-based authentication without the pitfalls",[22,23,24],"tldr",{},[18,25,26],{},"TL;DR (30 minutes):\nUse RS256 algorithm with strong keys, short expiration (15 min access, 7 day refresh), store tokens in httpOnly cookies (not localStorage), implement token rotation on refresh, validate all claims (iss, aud, exp), and maintain a token blacklist for revocation.",[18,28,29],{},"Prerequisites:",[31,32,33,37,40],"ul",{},[34,35,36],"li",{},"Understanding of authentication concepts",[34,38,39],{},"Node.js or similar backend",[34,41,42],{},"A way to generate/store secrets or key pairs",[44,45,47],"h2",{"id":46},"why-this-matters","Why This Matters",[18,49,50],{},"JWTs are commonly used but frequently misconfigured. The \"none\" algorithm attack, weak secrets, storing tokens in localStorage, and missing validation have led to countless security breaches. This guide covers secure JWT implementation.",[44,52,54],{"id":53},"step-by-step-guide","Step-by-Step Guide",[56,57,59,64],"step",{"number":58},"1",[60,61,63],"h3",{"id":62},"choose-the-right-signing-algorithm","Choose the right signing algorithm",[65,66,71],"pre",{"className":67,"code":69,"language":70},[68],"language-text","// AVOID: HS256 with weak secrets\n// AVOID: The \"none\" algorithm\n// GOOD: RS256 (RSA) or ES256 (ECDSA) for production\n\nimport { generateKeyPairSync } from 'crypto';\n\n// Generate RSA key pair (do this once, store securely)\nconst { publicKey, privateKey } = generateKeyPairSync('rsa', {\n  modulusLength: 2048,\n  publicKeyEncoding: { type: 'spki', format: 'pem' },\n  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }\n});\n\n// Or for ECDSA (smaller tokens)\nconst { publicKey: ecPublicKey, privateKey: ecPrivateKey } =\n  generateKeyPairSync('ec', {\n    namedCurve: 'P-256',\n    publicKeyEncoding: { type: 'spki', format: 'pem' },\n    privateKeyEncoding: { type: 'pkcs8', format: 'pem' }\n  });\n\n// Store keys in environment variables or secrets manager\n// Never commit private keys to git!\n","text",[72,73,69],"code",{"__ignoreMap":74},"",[56,76,78,82],{"number":77},"2",[60,79,81],{"id":80},"create-tokens-with-proper-claims","Create tokens with proper claims",[65,83,86],{"className":84,"code":85,"language":70},[68],"import jwt from 'jsonwebtoken';\n\nconst ACCESS_TOKEN_EXPIRY = '15m';  // Short-lived\nconst REFRESH_TOKEN_EXPIRY = '7d';   // Longer-lived\n\nfunction createTokens(user) {\n  const accessToken = jwt.sign(\n    {\n      sub: user.id,              // Subject (user ID)\n      email: user.email,\n      role: user.role\n    },\n    process.env.JWT_PRIVATE_KEY,\n    {\n      algorithm: 'RS256',\n      expiresIn: ACCESS_TOKEN_EXPIRY,\n      issuer: 'https://myapp.com',    // Who issued the token\n      audience: 'https://myapp.com',   // Who can use it\n      jwtid: crypto.randomUUID()       // Unique token ID\n    }\n  );\n\n  const refreshToken = jwt.sign(\n    {\n      sub: user.id,\n      type: 'refresh'\n    },\n    process.env.JWT_PRIVATE_KEY,\n    {\n      algorithm: 'RS256',\n      expiresIn: REFRESH_TOKEN_EXPIRY,\n      issuer: 'https://myapp.com',\n      jwtid: crypto.randomUUID()\n    }\n  );\n\n  // Store refresh token hash in database for revocation\n  await db.refreshToken.create({\n    data: {\n      userId: user.id,\n      tokenHash: hashToken(refreshToken),\n      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)\n    }\n  });\n\n  return { accessToken, refreshToken };\n}\n",[72,87,85],{"__ignoreMap":74},[56,89,91,95],{"number":90},"3",[60,92,94],{"id":93},"validate-tokens-properly","Validate tokens properly",[65,96,99],{"className":97,"code":98,"language":70},[68],"import jwt from 'jsonwebtoken';\n\nasync function validateAccessToken(token) {\n  try {\n    const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY, {\n      algorithms: ['RS256'],        // CRITICAL: Specify allowed algorithms\n      issuer: 'https://myapp.com',  // Verify issuer\n      audience: 'https://myapp.com', // Verify audience\n      complete: true                 // Get header info too\n    });\n\n    // Check if token is blacklisted (for revocation)\n    const isBlacklisted = await db.blacklistedToken.findUnique({\n      where: { jti: decoded.payload.jti }\n    });\n\n    if (isBlacklisted) {\n      throw new Error('Token has been revoked');\n    }\n\n    return decoded.payload;\n  } catch (error) {\n    if (error.name === 'TokenExpiredError') {\n      throw new Error('Token expired');\n    }\n    if (error.name === 'JsonWebTokenError') {\n      throw new Error('Invalid token');\n    }\n    throw error;\n  }\n}\n\n// Middleware\nasync function authMiddleware(req, res, next) {\n  const token = req.cookies.accessToken;  // From httpOnly cookie\n\n  if (!token) {\n    return res.status(401).json({ error: 'No token provided' });\n  }\n\n  try {\n    const user = await validateAccessToken(token);\n    req.user = user;\n    next();\n  } catch (error) {\n    return res.status(401).json({ error: error.message });\n  }\n}\n",[72,100,98],{"__ignoreMap":74},[56,102,104,108],{"number":103},"4",[60,105,107],{"id":106},"store-tokens-securely","Store tokens securely",[65,109,112],{"className":110,"code":111,"language":70},[68],"// WRONG: localStorage is vulnerable to XSS\nlocalStorage.setItem('token', accessToken);\n\n// CORRECT: Use httpOnly cookies (server sets them)\nfunction setAuthCookies(res, accessToken, refreshToken) {\n  // Access token cookie\n  res.cookie('accessToken', accessToken, {\n    httpOnly: true,     // Can't be accessed by JavaScript\n    secure: true,       // HTTPS only\n    sameSite: 'strict', // Prevent CSRF\n    maxAge: 15 * 60 * 1000,  // 15 minutes\n    path: '/'\n  });\n\n  // Refresh token cookie\n  res.cookie('refreshToken', refreshToken, {\n    httpOnly: true,\n    secure: true,\n    sameSite: 'strict',\n    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days\n    path: '/api/auth/refresh'  // Only sent to refresh endpoint\n  });\n}\n\n// Clear cookies on logout\nfunction clearAuthCookies(res) {\n  res.clearCookie('accessToken');\n  res.clearCookie('refreshToken', { path: '/api/auth/refresh' });\n}\n",[72,113,111],{"__ignoreMap":74},[56,115,117,121],{"number":116},"5",[60,118,120],{"id":119},"implement-refresh-token-rotation","Implement refresh token rotation",[65,122,125],{"className":123,"code":124,"language":70},[68],"async function refreshAccessToken(req, res) {\n  const refreshToken = req.cookies.refreshToken;\n\n  if (!refreshToken) {\n    return res.status(401).json({ error: 'No refresh token' });\n  }\n\n  try {\n    // Verify refresh token\n    const decoded = jwt.verify(refreshToken, process.env.JWT_PUBLIC_KEY, {\n      algorithms: ['RS256'],\n      issuer: 'https://myapp.com'\n    });\n\n    // Check if refresh token exists in database\n    const storedToken = await db.refreshToken.findFirst({\n      where: {\n        userId: decoded.sub,\n        tokenHash: hashToken(refreshToken),\n        expiresAt: { gt: new Date() }\n      }\n    });\n\n    if (!storedToken) {\n      // Token reuse detected! Revoke all user's tokens\n      await db.refreshToken.deleteMany({\n        where: { userId: decoded.sub }\n      });\n      clearAuthCookies(res);\n      return res.status(401).json({\n        error: 'Invalid refresh token. Please login again.'\n      });\n    }\n\n    // Delete old refresh token (rotation)\n    await db.refreshToken.delete({\n      where: { id: storedToken.id }\n    });\n\n    // Get user and create new tokens\n    const user = await db.user.findUnique({\n      where: { id: decoded.sub }\n    });\n\n    const { accessToken, refreshToken: newRefreshToken } = createTokens(user);\n    setAuthCookies(res, accessToken, newRefreshToken);\n\n    return res.json({ success: true });\n  } catch (error) {\n    clearAuthCookies(res);\n    return res.status(401).json({ error: 'Invalid refresh token' });\n  }\n}\n",[72,126,124],{"__ignoreMap":74},[56,128,130,134],{"number":129},"6",[60,131,133],{"id":132},"handle-token-revocation","Handle token revocation",[65,135,138],{"className":136,"code":137,"language":70},[68],"// For immediate revocation (logout, password change, etc.)\nasync function revokeAllUserTokens(userId) {\n  // Delete all refresh tokens\n  await db.refreshToken.deleteMany({\n    where: { userId }\n  });\n\n  // For access tokens, we need a blacklist (they're stateless)\n  // Option 1: Blacklist specific tokens until they expire\n  // Option 2: Track a \"tokens valid after\" timestamp per user\n}\n\n// Logout endpoint\nasync function logout(req, res) {\n  const accessToken = req.cookies.accessToken;\n  const refreshToken = req.cookies.refreshToken;\n\n  if (accessToken) {\n    // Blacklist access token until expiry\n    const decoded = jwt.decode(accessToken);\n    if (decoded?.jti && decoded?.exp) {\n      await db.blacklistedToken.create({\n        data: {\n          jti: decoded.jti,\n          expiresAt: new Date(decoded.exp * 1000)\n        }\n      });\n    }\n  }\n\n  if (refreshToken) {\n    // Delete refresh token from database\n    await db.refreshToken.deleteMany({\n      where: { tokenHash: hashToken(refreshToken) }\n    });\n  }\n\n  clearAuthCookies(res);\n  return res.json({ success: true });\n}\n\n// Clean up expired blacklist entries (run periodically)\nasync function cleanupBlacklist() {\n  await db.blacklistedToken.deleteMany({\n    where: { expiresAt: { lt: new Date() } }\n  });\n}\n",[72,139,137],{"__ignoreMap":74},[141,142,143,146],"warning-box",{},[18,144,145],{},"JWT Security Mistakes to Avoid:",[31,147,148,155,160,165,170,175],{},[34,149,150,154],{},[151,152,153],"strong",{},"Never"," allow the \"none\" algorithm - always specify allowed algorithms",[34,156,157,159],{},[151,158,153],{}," store JWTs in localStorage - use httpOnly cookies",[34,161,162,164],{},[151,163,153],{}," use weak secrets for HS256 - minimum 256 bits of entropy",[34,166,167,169],{},[151,168,153],{}," put sensitive data in JWT payload - it's base64, not encrypted",[34,171,172,174],{},[151,173,153],{}," skip validation - always verify signature, exp, iss, aud",[34,176,177,179],{},[151,178,153],{}," use long-lived tokens without refresh - max 15-30 minutes",[44,181,183],{"id":182},"how-to-verify-it-worked","How to Verify It Worked",[185,186,187,193,199,205],"ol",{},[34,188,189,192],{},[151,190,191],{},"Test algorithm confusion:"," Send a token with alg=\"none\" - should be rejected",[34,194,195,198],{},[151,196,197],{},"Test expired tokens:"," Wait for expiry, token should be rejected",[34,200,201,204],{},[151,202,203],{},"Test refresh rotation:"," Use a refresh token twice - second should fail",[34,206,207,210],{},[151,208,209],{},"Test revocation:"," Logout, then try the old token - should fail",[44,212,214],{"id":213},"common-errors-troubleshooting","Common Errors & Troubleshooting",[216,217,219],"h4",{"id":218},"error-invalid-algorithm","Error: \"invalid algorithm\"",[18,221,222],{},"You're verifying with a different algorithm than used for signing. Check that algorithms array matches.",[216,224,226],{"id":225},"error-invalid-signature","Error: \"invalid signature\"",[18,228,229],{},"Key mismatch. For RS256, sign with private key, verify with public key.",[216,231,233],{"id":232},"tokens-not-being-sent","Tokens not being sent",[18,235,236],{},"Check cookie settings - sameSite, secure, and path must be correct for your setup.",[216,238,240],{"id":239},"token-too-large","Token too large",[18,242,243],{},"JWTs in cookies have ~4KB limit. Don't put large data in tokens - use user ID and fetch data as needed.",[245,246,247,254,260],"faq-section",{},[248,249,251],"faq-item",{"question":250},"Why not just use sessions instead?",[18,252,253],{},"Sessions are often simpler and more secure for traditional web apps. JWTs make sense for: stateless microservices, mobile apps, or when you need tokens to work across domains. For most web apps, sessions are fine.",[248,255,257],{"question":256},"What's the deal with HS256 vs RS256?",[18,258,259],{},"HS256 uses a shared secret (both sides need the same key). RS256 uses public/private keys (sign with private, verify with public). RS256 is better when you have multiple services - they can verify tokens without having the signing key.",[248,261,263],{"question":262},"How do I handle token refresh in a SPA?",[18,264,265],{},"The access token cookie is sent automatically. When you get a 401, call your /refresh endpoint. If that fails, redirect to login. Use an interceptor in your HTTP client to handle this automatically.",[18,267,268,271,276,277,276,281],{},[151,269,270],{},"Related guides:",[272,273,275],"a",{"href":274},"/blog/how-to/session-management","Session Management"," ·\n",[272,278,280],{"href":279},"/blog/how-to/secure-login-form","Secure Login Form",[272,282,284],{"href":283},"/blog/how-to/oauth-setup","OAuth Setup",[286,287,288,294,299],"related-articles",{},[289,290],"related-card",{"description":291,"href":292,"title":293},"Step-by-step guide to adding secure authentication to Next.js apps. NextAuth setup, middleware protection, session handl","/blog/how-to/add-authentication-nextjs","How to Add Secure Authentication to Next.js",[289,295],{"description":296,"href":297,"title":298},"Step-by-step guide to adding security headers. Protect against XSS, clickjacking, and MIME sniffing with CSP, X-Frame-Op","/blog/how-to/add-security-headers","How to Add Security Headers to Your Web App",[289,300],{"description":301,"href":302,"title":303},"Comprehensive guide to API key security. Learn storage, rotation, scoping, monitoring, and incident response best practi","/blog/how-to/api-key-best-practices","API Key Security Best Practices",{"title":74,"searchDepth":305,"depth":305,"links":306},2,[307,308,317,318],{"id":46,"depth":305,"text":47},{"id":53,"depth":305,"text":54,"children":309},[310,312,313,314,315,316],{"id":62,"depth":311,"text":63},3,{"id":80,"depth":311,"text":81},{"id":93,"depth":311,"text":94},{"id":106,"depth":311,"text":107},{"id":119,"depth":311,"text":120},{"id":132,"depth":311,"text":133},{"id":182,"depth":305,"text":183},{"id":213,"depth":305,"text":214},"how-to","2026-01-19","Step-by-step guide to secure JWT implementation. Choose the right algorithm, handle token storage, implement refresh tokens, and avoid common vulnerabilities.",false,"md",null,"yellow",{},true,"Secure JWT implementation patterns and best practices.","/blog/how-to/jwt-security","[object Object]","HowTo",{"title":5,"description":321},{"loc":329},"blog/how-to/jwt-security",[],"summary_large_image","YWo3BL9n28nlpQ81O1lngPGtGg1PJj2lyvlcqyrIAt8",1775843928200]