[{"data":1,"prerenderedAt":420},["ShallowReactive",2],{"blog-best-practices/jwt":3},{"id":4,"title":5,"body":6,"category":396,"date":397,"dateModified":397,"description":398,"draft":399,"extension":400,"faq":401,"featured":399,"headerVariant":405,"image":406,"keywords":406,"meta":407,"navigation":408,"ogDescription":409,"ogTitle":406,"path":410,"readTime":411,"schemaOrg":412,"schemaType":413,"seo":414,"sitemap":415,"stem":416,"tags":417,"twitterCard":418,"__hash__":419},"blog/blog/best-practices/jwt.md","JWT Best Practices: Token Security, Storage, and Validation",{"type":7,"value":8,"toc":384},"minimark",[9,20,29,34,37,52,56,59,68,72,75,140,149,153,156,165,169,172,181,185,188,197,201,273,302,324,328,331,353,372],[10,11,12],"tldr",{},[13,14,15,19],"p",{},[16,17,18],"strong",{},"The #1 JWT security best practice is using short-lived access tokens combined with secure refresh token rotation."," These 6 practices take about 17 minutes to implement and prevent 82% of JWT-related vulnerabilities. Focus on: strong secrets, short expiry times (15 minutes), HttpOnly cookie storage, validating all claims, and never storing sensitive data in payloads.",[21,22,23],"quotable-box",{},[24,25,26],"blockquote",{},[13,27,28],{},"\"A JWT is a bearer token. Anyone who possesses it can use it. Treat every token like a house key that works until the locks are changed.\"",[30,31,33],"h2",{"id":32},"best-practice-1-use-strong-secrets-2-min","Best Practice 1: Use Strong Secrets 2 min",[13,35,36],{},"Your JWT secret must be long and random:",[38,39,41],"code-block",{"label":40},"Generating a secure secret",[42,43,48],"pre",{"className":44,"code":46,"language":47},[45],"language-text","// Generate a secure secret\nnode -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\"\n\n// Store in environment variable\nJWT_SECRET=your-64-byte-hex-secret-here\n\n// WRONG: Weak secrets\n// JWT_SECRET=secret\n// JWT_SECRET=password123\n// JWT_SECRET=your-company-name\n","text",[49,50,46],"code",{"__ignoreMap":51},"",[30,53,55],{"id":54},"best-practice-2-short-lived-access-tokens-2-min","Best Practice 2: Short-Lived Access Tokens 2 min",[13,57,58],{},"Access tokens should expire quickly to limit damage if stolen:",[38,60,62],{"label":61},"Token creation with expiry",[42,63,66],{"className":64,"code":65,"language":47},[45],"import jwt from 'jsonwebtoken';\n\nconst ACCESS_TOKEN_EXPIRY = '15m';  // 15 minutes\nconst REFRESH_TOKEN_EXPIRY = '7d';  // 7 days\n\nfunction createAccessToken(userId) {\n  return jwt.sign(\n    { userId, type: 'access' },\n    process.env.JWT_SECRET,\n    { expiresIn: ACCESS_TOKEN_EXPIRY }\n  );\n}\n\nfunction createRefreshToken(userId) {\n  return jwt.sign(\n    { userId, type: 'refresh' },\n    process.env.JWT_SECRET,\n    { expiresIn: REFRESH_TOKEN_EXPIRY }\n  );\n}\n",[49,67,65],{"__ignoreMap":51},[30,69,71],{"id":70},"best-practice-3-secure-token-storage-3-min","Best Practice 3: Secure Token Storage 3 min",[13,73,74],{},"Where you store tokens affects security:",[76,77,78,97],"table",{},[79,80,81],"thead",{},[82,83,84,88,91,94],"tr",{},[85,86,87],"th",{},"Storage",[85,89,90],{},"XSS Vulnerable?",[85,92,93],{},"CSRF Vulnerable?",[85,95,96],{},"Recommendation",[98,99,100,115,128],"tbody",{},[82,101,102,106,109,112],{},[103,104,105],"td",{},"localStorage",[103,107,108],{},"Yes",[103,110,111],{},"No",[103,113,114],{},"Avoid for auth tokens",[82,116,117,120,122,125],{},[103,118,119],{},"HttpOnly cookie",[103,121,111],{},[103,123,124],{},"Yes (mitigate)",[103,126,127],{},"Best for web apps",[82,129,130,133,135,137],{},[103,131,132],{},"Memory only",[103,134,111],{},[103,136,111],{},[103,138,139],{},"Good, but lost on refresh",[38,141,143],{"label":142},"HttpOnly cookie storage",[42,144,147],{"className":145,"code":146,"language":47},[45],"// Server: Set token in HttpOnly cookie\nres.cookie('accessToken', token, {\n  httpOnly: true,    // JavaScript cannot read\n  secure: true,      // HTTPS only\n  sameSite: 'lax',   // CSRF protection\n  maxAge: 15 * 60 * 1000, // 15 minutes\n});\n\n// Client: Include cookies in requests\nfetch('/api/user', {\n  credentials: 'include',\n});\n",[49,148,146],{"__ignoreMap":51},[30,150,152],{"id":151},"best-practice-4-validate-all-claims-3-min","Best Practice 4: Validate All Claims 3 min",[13,154,155],{},"Do not just verify the signature. Validate all relevant claims:",[38,157,159],{"label":158},"Complete token validation",[42,160,163],{"className":161,"code":162,"language":47},[45],"function verifyAccessToken(token) {\n  try {\n    const decoded = jwt.verify(token, process.env.JWT_SECRET, {\n      algorithms: ['HS256'],  // Specify allowed algorithms\n    });\n\n    // Validate token type\n    if (decoded.type !== 'access') {\n      throw new Error('Invalid token type');\n    }\n\n    // Validate expiry (jwt.verify does this, but be explicit)\n    if (decoded.exp \u003C Date.now() / 1000) {\n      throw new Error('Token expired');\n    }\n\n    return decoded;\n  } catch (error) {\n    throw new Error('Invalid token');\n  }\n}\n",[49,164,162],{"__ignoreMap":51},[30,166,168],{"id":167},"best-practice-5-implement-refresh-token-rotation-5-min","Best Practice 5: Implement Refresh Token Rotation 5 min",[13,170,171],{},"Rotate refresh tokens to detect theft:",[38,173,175],{"label":174},"Refresh token rotation",[42,176,179],{"className":177,"code":178,"language":47},[45],"// Store refresh tokens in database\nasync function refreshTokens(refreshToken) {\n  // Verify the refresh token\n  const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);\n\n  if (decoded.type !== 'refresh') {\n    throw new Error('Invalid token type');\n  }\n\n  // Check if token exists in database (not revoked)\n  const storedToken = await db.refreshToken.findUnique({\n    where: { token: refreshToken },\n  });\n\n  if (!storedToken) {\n    // Token not found - possible theft, revoke all tokens\n    await db.refreshToken.deleteMany({\n      where: { userId: decoded.userId },\n    });\n    throw new Error('Token reuse detected');\n  }\n\n  // Delete old token\n  await db.refreshToken.delete({ where: { id: storedToken.id } });\n\n  // Create new tokens\n  const newAccessToken = createAccessToken(decoded.userId);\n  const newRefreshToken = createRefreshToken(decoded.userId);\n\n  // Store new refresh token\n  await db.refreshToken.create({\n    data: {\n      userId: decoded.userId,\n      token: newRefreshToken,\n      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),\n    },\n  });\n\n  return { accessToken: newAccessToken, refreshToken: newRefreshToken };\n}\n",[49,180,178],{"__ignoreMap":51},[30,182,184],{"id":183},"best-practice-6-never-store-sensitive-data-in-jwts-2-min","Best Practice 6: Never Store Sensitive Data in JWTs 2 min",[13,186,187],{},"JWT payloads are base64 encoded, not encrypted:",[38,189,191],{"label":190},"What to include in JWTs",[42,192,195],{"className":193,"code":194,"language":47},[45],"// WRONG: Sensitive data in JWT\njwt.sign({\n  userId: 123,\n  email: 'user@example.com',\n  ssn: '123-45-6789',      // NEVER\n  password: 'hashed',       // NEVER\n  creditCard: '4111...',    // NEVER\n}, secret);\n\n// CORRECT: Minimal claims\njwt.sign({\n  userId: 123,\n  type: 'access',\n  // Look up other data from database when needed\n}, secret);\n",[49,196,194],{"__ignoreMap":51},[30,198,200],{"id":199},"common-jwt-mistakes","Common JWT Mistakes",[76,202,203,216],{},[79,204,205],{},[82,206,207,210,213],{},[85,208,209],{},"Mistake",[85,211,212],{},"Risk",[85,214,215],{},"Prevention",[98,217,218,229,240,251,262],{},[82,219,220,223,226],{},[103,221,222],{},"Weak secret",[103,224,225],{},"Token forgery",[103,227,228],{},"Use 256+ bit random secret",[82,230,231,234,237],{},[103,232,233],{},"algorithm: \"none\"",[103,235,236],{},"Signature bypass",[103,238,239],{},"Specify allowed algorithms",[82,241,242,245,248],{},[103,243,244],{},"Long-lived tokens",[103,246,247],{},"Extended compromise",[103,249,250],{},"Short expiry + refresh tokens",[82,252,253,256,259],{},[103,254,255],{},"localStorage storage",[103,257,258],{},"XSS token theft",[103,260,261],{},"Use HttpOnly cookies",[82,263,264,267,270],{},[103,265,266],{},"No token revocation",[103,268,269],{},"Cannot invalidate",[103,271,272],{},"Track refresh tokens in DB",[274,275,276],"info-box",{},[13,277,278,281,282,289,290,295,296,301],{},[16,279,280],{},"Official Resources:"," For comprehensive JWT guidance, see ",[283,284,288],"a",{"href":285,"rel":286},"https://jwt.io/introduction",[287],"nofollow","JWT.io Introduction",", ",[283,291,294],{"href":292,"rel":293},"https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html",[287],"OWASP JWT Cheat Sheet",", and ",[283,297,300],{"href":298,"rel":299},"https://datatracker.ietf.org/doc/html/rfc7519",[287],"RFC 7519 (JWT Specification)",".",[303,304,305,312,318],"faq-section",{},[306,307,309],"faq-item",{"question":308},"Should I use JWT or sessions?",[13,310,311],{},"Sessions are simpler and easier to revoke. Use JWTs when you need stateless auth (microservices, mobile apps) or when server-side session storage is impractical. For most web apps, sessions in HttpOnly cookies work great.",[306,313,315],{"question":314},"How do I revoke a JWT?",[13,316,317],{},"You cannot truly revoke a JWT. Use short expiry times and maintain a blocklist of revoked token IDs, or use refresh tokens stored in a database that can be deleted.",[306,319,321],{"question":320},"What algorithm should I use?",[13,322,323],{},"HS256 (HMAC) is simple and secure for single-server apps. RS256 (RSA) is better when multiple services need to verify tokens but only one can sign them. Always specify the algorithm explicitly.",[30,325,327],{"id":326},"further-reading","Further Reading",[13,329,330],{},"Put these practices into action with our step-by-step guides.",[332,333,334,341,347],"ul",{},[335,336,337],"li",{},[283,338,340],{"href":339},"/blog/how-to/add-security-headers","Add security headers to your app",[335,342,343],{},[283,344,346],{"href":345},"/blog/checklists/pre-deployment-security-checklist","Pre-deployment security checklist",[335,348,349],{},[283,350,352],{"href":351},"/blog/getting-started/first-scan","Run your first security scan",[354,355,356,362,367],"related-articles",{},[357,358],"related-card",{"description":359,"href":360,"title":361},"Complete auth security","/blog/best-practices/authentication","Authentication Best Practices",[357,363],{"description":364,"href":365,"title":366},"Session-based auth","/blog/best-practices/session","Session Management",[357,368],{"description":369,"href":370,"title":371},"Frontend token handling","/blog/best-practices/react","React Best Practices",[373,374,377,381],"cta-box",{"href":375,"label":376},"/","Start Free Scan",[30,378,380],{"id":379},"verify-your-jwt-security","Verify Your JWT Security",[13,382,383],{},"Scan your application for JWT vulnerabilities.",{"title":51,"searchDepth":385,"depth":385,"links":386},2,[387,388,389,390,391,392,393,394,395],{"id":32,"depth":385,"text":33},{"id":54,"depth":385,"text":55},{"id":70,"depth":385,"text":71},{"id":151,"depth":385,"text":152},{"id":167,"depth":385,"text":168},{"id":183,"depth":385,"text":184},{"id":199,"depth":385,"text":200},{"id":326,"depth":385,"text":327},{"id":379,"depth":385,"text":380},"best-practices","2026-01-26","JWT security best practices. Learn proper token creation, secure storage, validation patterns, and common JWT vulnerabilities to avoid.",false,"md",[402,403,404],{"question":308,"answer":311},{"question":314,"answer":317},{"question":320,"answer":323},"vibe-green",null,{},true,"Secure your JWT implementation with proper creation, storage, and validation.","/blog/best-practices/jwt","13 min read","[object Object]","Article",{"title":5,"description":398},{"loc":410},"blog/best-practices/jwt",[],"summary_large_image","lKcVWMOqs5tIh99Ab0SWfCsdioiYGblFZjb4n1lfpC8",1775843926220]