<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SHAHJAHAN MD. SWAJAN</title>
    <description>The latest articles on DEV Community by SHAHJAHAN MD. SWAJAN (@swajannn).</description>
    <link>https://dev.to/swajannn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F393719%2Fc2e99e69-5b65-48f4-ad8c-2499c92f3ba9.png</url>
      <title>DEV Community: SHAHJAHAN MD. SWAJAN</title>
      <link>https://dev.to/swajannn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swajannn"/>
    <language>en</language>
    <item>
      <title>JWT Security Checklist: 12 Things to Verify Before You Ship</title>
      <dc:creator>SHAHJAHAN MD. SWAJAN</dc:creator>
      <pubDate>Mon, 27 Jul 2026 20:55:00 +0000</pubDate>
      <link>https://dev.to/swajannn/jwt-security-checklist-12-things-to-verify-before-you-ship-3kp</link>
      <guid>https://dev.to/swajannn/jwt-security-checklist-12-things-to-verify-before-you-ship-3kp</guid>
      <description>&lt;p&gt;JWT authentication has more failure modes than most developers realise. Correct signature verification is necessary but far from sufficient. This checklist is what I run through before every production JWT deployment.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Secret Is Generated With a CSPRNG
&lt;/h4&gt;

&lt;p&gt;Not a password. Not a UUID. Not a timestamp. A cryptographically secure pseudorandom number generator output.&lt;/p&gt;

&lt;p&gt;In Node.js: &lt;code&gt;crypto.randomBytes(32).toString('hex')&lt;/code&gt;&lt;br&gt;&lt;br&gt;
In Python: &lt;code&gt;secrets.token_hex(32)&lt;/code&gt;&lt;br&gt;&lt;br&gt;
In the browser: &lt;a href="https://www.jwtsecretgenerator.com/tools/jwt-secret-generator" rel="noopener noreferrer"&gt;jwtsecretgenerator.com/tools/jwt-secret-generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 256-bit CSPRNG secret takes &lt;code&gt;10^59&lt;/code&gt; years to brute force at current GPU speeds.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Algorithm Is Explicitly Specified in verify()
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong&lt;/span&gt;
&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Right&lt;/span&gt;
&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. exp Claim Is Present and Validated
&lt;/h4&gt;

&lt;p&gt;Short-lived tokens (15 minutes) limit the damage from leaks. Verify your library is actually checking &lt;code&gt;exp&lt;/code&gt; — some require explicit configuration.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. iss and aud Claims Are Validated
&lt;/h4&gt;

&lt;p&gt;Validates the token was issued by your service and intended for your API. Prevents token reuse across services.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Tokens Are in httpOnly Cookies, Not localStorage
&lt;/h4&gt;

&lt;p&gt;localStorage is readable by any script on the page. httpOnly cookies are invisible to JavaScript.&lt;/p&gt;

&lt;h4&gt;
  
  
  6. HTTPS Is Enforced
&lt;/h4&gt;

&lt;p&gt;JWT in a query parameter over HTTP is visible in every proxy, CDN, and server log on the path. Use the &lt;code&gt;Authorization: Bearer&lt;/code&gt; header over HTTPS only.&lt;/p&gt;

&lt;h4&gt;
  
  
  7. Refresh Tokens Are Server-Side Revocable
&lt;/h4&gt;

&lt;p&gt;Short access tokens + server-side refresh tokens = the ability to end sessions immediately. Long-lived access tokens without refresh logic cannot be revoked.&lt;/p&gt;

&lt;h4&gt;
  
  
  8. The jti Claim Is Used If You Need Immediate Revocation
&lt;/h4&gt;

&lt;p&gt;Store revoked &lt;code&gt;jti&lt;/code&gt; values in Redis with TTL matching token expiry. Check on every request. Adds one Redis lookup per request — worth it for high-security endpoints.&lt;/p&gt;

&lt;h4&gt;
  
  
  9. Different Secrets for Each Environment
&lt;/h4&gt;

&lt;p&gt;Dev secret leaks should not compromise production. Keep them separate.&lt;/p&gt;

&lt;h4&gt;
  
  
  10. Secret Is Not in Source Code or Version Control
&lt;/h4&gt;

&lt;p&gt;Check: &lt;code&gt;git log -S "JWT_SECRET" -- .&lt;/code&gt; — if any results appear, rotate immediately.&lt;/p&gt;

&lt;h4&gt;
  
  
  11. Error Messages Do Not Reveal Why Verification Failed
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad — tells attacker whether to try a different token or different secret&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Good&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  12. Payload Does Not Contain Sensitive Data
&lt;/h4&gt;

&lt;p&gt;JWT payload is base64, not encrypted. Decode any JWT in 3 seconds at jwt.io. Never put passwords, full PII, or financial data in a JWT payload.&lt;/p&gt;

&lt;p&gt;The full version of this checklist with code examples for each point is at &lt;a href="https://www.jwtsecretgenerator.com/blog/jwt-security-checklist-2026" rel="noopener noreferrer"&gt;jwtsecretgenerator.com/blog/jwt-security-checklist-2026&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>devops</category>
      <category>jwt</category>
    </item>
    <item>
      <title>What Size JWT Secret Do I Need? Key Length Explained</title>
      <dc:creator>SHAHJAHAN MD. SWAJAN</dc:creator>
      <pubDate>Sat, 25 Jul 2026 21:02:00 +0000</pubDate>
      <link>https://dev.to/swajannn/what-size-jwt-secret-do-i-need-key-length-explained-13o0</link>
      <guid>https://dev.to/swajannn/what-size-jwt-secret-do-i-need-key-length-explained-13o0</guid>
      <description>&lt;p&gt;"How long should my JWT secret be?" is one of the most searched JWT security questions. The answer depends on your signing algorithm — but for the vast majority of applications, the answer is simple: 256 bits.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Short Answer&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Minimum Secret Size&lt;/th&gt;
&lt;th&gt;Hex Characters&lt;/th&gt;
&lt;th&gt;Bytes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-----------&lt;/td&gt;
&lt;td&gt;--------------------:&lt;/td&gt;
&lt;td&gt;---------------:&lt;/td&gt;
&lt;td&gt;------:&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HS256&lt;/td&gt;
&lt;td&gt;256 bits&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HS384&lt;/td&gt;
&lt;td&gt;384 bits&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HS512&lt;/td&gt;
&lt;td&gt;512 bits&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are using HS256 (the most common choice), generate a 256-bit (32-byte) random secret. That is 64 hexadecimal characters.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why 256 Bits Is Enough&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A properly random 256-bit key has 2^256 possible values. Even at a trillion guesses per second, brute-forcing a 256-bit secret would take longer than the age of the universe. NIST recommends a minimum of 128 bits for symmetric keys; 256 bits provides a comfortable security margin.&lt;/p&gt;

&lt;p&gt;The real risk is not key length — it is using predictable secrets, reusing secrets across environments, or storing them insecurely.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;When to Use 512 Bits&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Use a 512-bit secret when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are signing with HS512 (the HMAC block size aligns with 512-bit keys)
&lt;/li&gt;
&lt;li&gt;Compliance requirements mandate longer key material
&lt;/li&gt;
&lt;li&gt;You want defense-in-depth in high-security or regulated environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For standard HS256 production use, 512-bit secrets offer no practical security benefit over 256-bit — they just take more storage space.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How to Generate the Right Size&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Use the &lt;a href="https://www.jwtsecretgenerator.com/tools/jwt-secret-generator" rel="noopener noreferrer"&gt;JWT Secret Generator&lt;/a&gt; and select the preset that matches your algorithm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;128-bit — testing only, never production
&lt;/li&gt;
&lt;li&gt;256-bit — standard production (HS256)
&lt;/li&gt;
&lt;li&gt;512-bit — HS512 or high-security environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;// Node.js: 256-bit secret for HS256&lt;br&gt;&lt;br&gt;
const secret = require('crypto').randomBytes(32).toString('hex');&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What NOT to Use&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Passphrases or dictionary words ("my-super-secret-key")
&lt;/li&gt;
&lt;li&gt;UUIDs or short random strings (&amp;lt; 128 bits)
&lt;/li&gt;
&lt;li&gt;Base64-encoded short passwords
&lt;/li&gt;
&lt;li&gt;The same secret you use for other purposes (database encryption, API keys)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Bits, Bytes, and Characters Explained&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Developers often confuse these units. Here is how they map for hex-encoded secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;256 bits = 32 bytes = 64 hexadecimal characters
&lt;/li&gt;
&lt;li&gt;512 bits = 64 bytes = 128 hexadecimal characters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you see a 64-character hex string from the &lt;a href="https://www.jwtsecretgenerator.com/tools/jwt-secret-generator" rel="noopener noreferrer"&gt;JWT Secret Generator&lt;/a&gt;, you have 256 bits of entropy. A 32-character string is only 128 bits — half the recommended minimum for HS256 production use.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Compliance and Industry Guidance&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Standard / Source&lt;/th&gt;
&lt;th&gt;Minimum Symmetric Key Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NIST SP 800-131A&lt;/td&gt;
&lt;td&gt;128 bits (112 effective)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OWASP JWT Cheat Sheet&lt;/td&gt;
&lt;td&gt;256 bits for HS256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RFC 7518 (JWA)&lt;/td&gt;
&lt;td&gt;Key size &amp;gt;= hash output size&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Regulated industries (HIPAA, PCI-DSS, SOC2) often require documented key management procedures. Using 256-bit random secrets satisfies the cryptographic requirement — but auditors also expect rotation policies, access controls, and secrets manager usage. Read &lt;a href="https://www.jwtsecretgenerator.com/blog/how-to-store-jwt-secrets-securely" rel="noopener noreferrer"&gt;how to store JWT secrets securely&lt;/a&gt; for storage patterns that satisfy compliance reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Testing Your Key Size in Practice&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After generating a secret, sign a test token and verify it:&lt;/p&gt;

&lt;p&gt;const jwt = require('jsonwebtoken');&lt;br&gt;&lt;br&gt;
const secret = process.env.JWT_SECRET;&lt;br&gt;&lt;br&gt;
console.log('Secret length (hex chars):', secret.length); // expect 64 for 256-bit&lt;br&gt;&lt;br&gt;
const token = jwt.sign({ test: true }, secret, { algorithm: 'HS256', expiresIn: '5m' });&lt;br&gt;&lt;br&gt;
jwt.verify(token, secret, { algorithms: ['HS256'] }); // throws if invalid&lt;/p&gt;

&lt;p&gt;Paste the token into the &lt;a href="https://www.jwtsecretgenerator.com/tools/jwt-validator" rel="noopener noreferrer"&gt;JWT Validator&lt;/a&gt; to confirm the signature and inspect the decoded payload. If verification fails, check that your secret has no trailing whitespace or newline characters from copy-paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Real-World Scenarios&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Startup MVP: 256-bit HS256 secret in platform environment variables. Rotate manually when team members leave.&lt;/p&gt;

&lt;p&gt;Growing SaaS: Move to AWS Secrets Manager or Doppler. Automate rotation with the kid header. Keep 256-bit keys — upgrading to 512-bit adds no practical benefit for HS256.&lt;/p&gt;

&lt;p&gt;High-security API: HS512 with 512-bit secrets, short token TTL (15 minutes), refresh token rotation, and audience validation on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Verify Your Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After generating a secret, sign a test token and verify it with the &lt;a href="https://www.jwtsecretgenerator.com/tools/jwt-validator" rel="noopener noreferrer"&gt;JWT Validator&lt;/a&gt;. Read the full &lt;a href="https://www.jwtsecretgenerator.com/blog/jwt-secret-key-length" rel="noopener noreferrer"&gt;key length comparison&lt;/a&gt; for deeper technical detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Related Guides&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.jwtsecretgenerator.com/blog/how-to-generate-jwt-secret" rel="noopener noreferrer"&gt;How to generate a JWT secret&lt;/a&gt; — step-by-step walkthrough
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.jwtsecretgenerator.com/blog/what-is-a-jwt-secret-key" rel="noopener noreferrer"&gt;What is a JWT secret key&lt;/a&gt; — fundamentals
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.jwtsecretgenerator.com/blog/jwt-best-practices-checklist" rel="noopener noreferrer"&gt;JWT best practices checklist&lt;/a&gt; — production security&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Summary&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For HS256 — the default choice for most APIs — use a 256-bit (64-character hex) randomly generated secret. This applies to virtually every web application, mobile backend, and internal service. Upgrade to 512-bit only for HS512 or explicit compliance requirements. The &lt;a href="https://www.jwtsecretgenerator.com/tools/jwt-secret-generator" rel="noopener noreferrer"&gt;JWT Secret Generator&lt;/a&gt; defaults to the correct size for your selected algorithm. When in doubt, choose 256 bits in production. Shorter secrets are the most common cause of offline JWT cracking attacks.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Audited 12 Open Source JWT Implementations and Found the Same 6 Mistakes</title>
      <dc:creator>SHAHJAHAN MD. SWAJAN</dc:creator>
      <pubDate>Fri, 24 Jul 2026 20:47:11 +0000</pubDate>
      <link>https://dev.to/swajannn/i-audited-12-open-source-jwt-implementations-and-found-the-same-6-mistakes-3jj9</link>
      <guid>https://dev.to/swajannn/i-audited-12-open-source-jwt-implementations-and-found-the-same-6-mistakes-3jj9</guid>
      <description>&lt;p&gt;I spent last month reviewing JWT implementations across 12 open-source Node.js projects on GitHub — ranging from starter templates with 2k stars to production boilerplates used by teams at real companies. I found the same 6 mistakes in almost every one.&lt;/p&gt;

&lt;p&gt;None of these projects are bad. The developers are skilled. The mistakes are subtle, copy-paste errors from tutorials that nobody questioned.&lt;/p&gt;

&lt;p&gt;Here they are.&lt;/p&gt;

&lt;h4&gt;
  
  
  Mistake 1 — The Secret Is Literally &lt;code&gt;"secret"&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;I found this in three separate projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1h&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This secret is in every JWT tutorial on the internet. It is in the jwt.io documentation. It is in the jsonwebtoken README. Developers copy it and forget to replace it.&lt;/p&gt;

&lt;p&gt;A 6-character ASCII secret has approximately 42 bits of entropy. A GPU cluster cracks it from a dictionary in milliseconds. &lt;a href="https://www.jwtsecretgenerator.com/tools/jwt-secret-generator" rel="noopener noreferrer"&gt;Generate a real secret here&lt;/a&gt; — it takes 3 seconds and produces a 256-bit cryptographically random key.&lt;/p&gt;

&lt;h4&gt;
  
  
  Mistake 2 — &lt;code&gt;jwt.decode()&lt;/code&gt; Used in Auth Middleware
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DANGEROUS — this is in a production auth middleware&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jwt.decode()&lt;/code&gt; does not verify the signature. It reads the payload regardless of whether the token is valid, expired, or forged. An attacker can craft any payload they want and it will pass this check.&lt;/p&gt;

&lt;p&gt;The fix is two characters: &lt;code&gt;jwt.verify()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Mistake 3 — Algorithm Not Specified in &lt;code&gt;verify()&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Missing algorithms option&lt;/span&gt;
&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;{ algorithms: ['HS256'] }&lt;/code&gt;, the library trusts whatever algorithm is in the token's header. An attacker can create a token with &lt;code&gt;alg: none&lt;/code&gt; and an empty signature — and &lt;code&gt;jwt.verify()&lt;/code&gt; will accept it.&lt;/p&gt;

&lt;p&gt;Always specify the expected algorithm explicitly.&lt;/p&gt;

&lt;h4&gt;
  
  
  Mistake 4 — Secret Committed to Version Control
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Found in config.js, committed to a public repo&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;jwtSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;productionsecretdonotshare2024&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the comment — "do not share." The developer knew this was sensitive. But it still ended up committed to a public repository.&lt;/p&gt;

&lt;p&gt;Once in git history, a secret is compromised permanently. Even deleting the file does not remove it from history. Rotate immediately if this has happened to you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Mistake 5 — Tokens Stored in localStorage
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Frontend code in multiple projects&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Later:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; is accessible to any JavaScript running on the page. A single XSS vulnerability — an improperly sanitised comment, a compromised npm package, a third-party script — and all stored tokens are exfiltrated.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;httpOnly&lt;/code&gt; cookies for authentication tokens. They are invisible to JavaScript, making XSS token theft impossible.&lt;/p&gt;

&lt;h4&gt;
  
  
  Mistake 6 — Tokens Never Expire
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// No expiresIn — token is valid forever&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A JWT without an &lt;code&gt;exp&lt;/code&gt; claim never expires. If it is ever leaked — in a log file, an error response, a frontend cache — it remains valid indefinitely.&lt;/p&gt;

&lt;p&gt;Always set &lt;code&gt;expiresIn&lt;/code&gt;. For access tokens: 15 minutes. For long-lived sessions: use a refresh token pattern, not a long-lived access token.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Pre-Ship Checklist
&lt;/h4&gt;

&lt;p&gt;Before your next JWT implementation goes to production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Secret generated with a CSPRNG, minimum 256 bits&lt;/li&gt;
&lt;li&gt;[ ] Secret in environment variable, not in code&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;jwt.verify()&lt;/code&gt; used everywhere, never &lt;code&gt;jwt.decode()&lt;/code&gt; alone&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;{ algorithms: ['HS256'] }&lt;/code&gt; explicitly specified&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;expiresIn&lt;/code&gt; set on all tokens&lt;/li&gt;
&lt;li&gt;[ ] Tokens in &lt;code&gt;httpOnly&lt;/code&gt; cookies, not &lt;code&gt;localStorage&lt;/code&gt;
I have been using &lt;a href="https://www.jwtsecretgenerator.com/tools/jwt-secret-generator" rel="noopener noreferrer"&gt;jwtsecretgenerator.com&lt;/a&gt; for generating secrets — it runs entirely in the browser using the Web Crypto API, nothing is sent to a server, and it produces the right bit length for your algorithm.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>jwtsecret</category>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Audited 12 Open Source Projects' JWT Implementations and Found the Same 6 Mistakes in All of Them</title>
      <dc:creator>SHAHJAHAN MD. SWAJAN</dc:creator>
      <pubDate>Wed, 22 Apr 2026 19:37:47 +0000</pubDate>
      <link>https://dev.to/swajannn/i-audited-12-open-source-projects-jwt-implementations-and-found-the-same-6-mistakes-in-all-of-them-4058</link>
      <guid>https://dev.to/swajannn/i-audited-12-open-source-projects-jwt-implementations-and-found-the-same-6-mistakes-in-all-of-them-4058</guid>
      <description>&lt;p&gt;It started with a throwaway comment in a code review.&lt;/p&gt;

&lt;p&gt;I was scanning through a popular Node.js starter template on GitHub — one with over 4,000 stars, regularly featured in "best Express boilerplate" roundup articles. The kind of repo junior developers clone on day one and senior developers use as a starting point for new services. And buried inside &lt;code&gt;config/default.js&lt;/code&gt;, I found this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;jwtSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;jwtExpiry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;7d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a placeholder. Not a comment warning you to change it. Just... &lt;code&gt;"secret"&lt;/code&gt;. Shipped to production in thousands of forks.&lt;/p&gt;

&lt;p&gt;I closed the tab, then reopened it. Then I opened eleven more repos.&lt;/p&gt;

&lt;p&gt;What I found over the next few hours was not reassuring. The same class of mistakes appeared again and again — not just in beginner repos, but in widely-used templates, production starter kits, and even a few packages with active contributors who clearly knew what they were doing. JWT is one of those technologies where the happy path works fine and the failure modes are invisible until someone is already inside your system.&lt;/p&gt;

&lt;p&gt;Here are the six mistakes I found, what they look like in real code, why they matter, and how to fix them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 12 Repos: A Quick Note on Scope
&lt;/h2&gt;

&lt;p&gt;To keep this constructive rather than a public callout, I'm not naming repositories directly. Instead I'll describe them by category. The sample included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 Express + MongoDB boilerplates (2,000–8,000 stars each)&lt;/li&gt;
&lt;li&gt;2 NestJS starter kits&lt;/li&gt;
&lt;li&gt;2 full-stack Next.js templates with built-in auth&lt;/li&gt;
&lt;li&gt;1 Fastify REST API scaffold&lt;/li&gt;
&lt;li&gt;1 Node.js microservices example repo from a major cloud provider's tutorial series&lt;/li&gt;
&lt;li&gt;1 React Native + Node backend starter&lt;/li&gt;
&lt;li&gt;1 GraphQL + Apollo Server template&lt;/li&gt;
&lt;li&gt;1 open-source SaaS boilerplate with a paid tier
All were JavaScript or TypeScript. All used &lt;code&gt;jsonwebtoken&lt;/code&gt; or a thin wrapper around it. All had at least one of the six mistakes below. Eight had three or more.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Mistake 1 — The Hardcoded Static Secret
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frequency: 10 of 12 repos&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The most common pattern I found&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsonwebtoken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mysecret123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Variations I encountered: &lt;code&gt;"secret"&lt;/code&gt;, &lt;code&gt;"jwt_secret"&lt;/code&gt;, &lt;code&gt;"your-secret-key"&lt;/code&gt;, &lt;code&gt;"supersecret"&lt;/code&gt;, &lt;code&gt;"changeme"&lt;/code&gt;, and my personal favourite, &lt;code&gt;"TODO_REPLACE_THIS"&lt;/code&gt; — which had not been replaced in any of the commits going back two years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's dangerous:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The security of HMAC-based JWT (HS256, HS384, HS512) depends entirely on the unpredictability of the secret. If an attacker knows or guesses the secret, they can forge any token — including &lt;code&gt;{ "role": "admin" }&lt;/code&gt; tokens for users that don't exist.&lt;/p&gt;

&lt;p&gt;A string like &lt;code&gt;"mysecret123"&lt;/code&gt; has roughly 65 bits of entropy in theory, but in practice it's a dictionary target. Security researchers maintain public lists of common JWT secrets scraped from GitHub, Stack Overflow, and tutorial sites. If your secret appears in that list — and &lt;code&gt;"secret"&lt;/code&gt;, &lt;code&gt;"mysecret"&lt;/code&gt;, and &lt;code&gt;"jwt_secret"&lt;/code&gt; absolutely do — a brute-force attack with a modern GPU takes seconds, not years.&lt;/p&gt;

&lt;p&gt;For HS256, you need a minimum of 256 bits (32 bytes) of cryptographically random data. Not a memorable string. Not a UUID. Not your app name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generate once using Node's crypto module&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// outputs: 3f2a1b9c4d8e7f6a... (64 hex chars = 256 bits)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store this in an environment variable. Never in source code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In production&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JWT_SECRET must be set and at least 32 characters&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need a production-grade 256-bit secret right now, &lt;a href="https://jwtsecretgenerator.com/tools/jwt-secret-generator" rel="noopener noreferrer"&gt;jwtsecretgenerator.com/tools/jwt-secret-generator&lt;/a&gt; generates one client-side using the Web Crypto API — nothing leaves your browser. It's the fastest way to get a properly random secret with zero server-side risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 2 — The &lt;code&gt;algorithm: "none"&lt;/code&gt; Attack Surface
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frequency: 7 of 12 repos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The classic JWT algorithm confusion attack. Here's the vulnerable pattern I found repeatedly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vulnerable — no algorithm allowlist&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker who can intercept or modify a token can change the header from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then strip the signature entirely. Older versions of some JWT libraries — and &lt;code&gt;jsonwebtoken&lt;/code&gt; without explicit algorithm enforcement — will verify this token as valid, because a "none" algorithm means "no signature required." The attacker has just granted themselves any claims they want.&lt;/p&gt;

&lt;p&gt;Here's what a decoded &lt;code&gt;alg: none&lt;/code&gt; attack payload looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(base&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="err"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;decoded)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(base&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="err"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;decoded)&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1714000000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Signature&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;(empty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;eyJhbGciOiJub&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="err"&gt;lIiwidHlwIjoiSldUIn&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;.eyJ&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;VySWQiOiIxIiwicm&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;sZSI&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;ImFkbWluIiwiaWF&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;IjoxNzE&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;MDAwMDAwfQ.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Always pass an explicit &lt;code&gt;algorithms&lt;/code&gt; allowlist to &lt;code&gt;jwt.verify()&lt;/code&gt;. This is not optional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Safe — explicit algorithm allowlist&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// NEVER include "none"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use RS256 (asymmetric), explicitly allow only &lt;code&gt;["RS256"]&lt;/code&gt;. Never use an empty array or omit the option entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 3 — Secrets Committed to Git
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frequency: 6 of 12 repos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few repos had cleaned up their secrets into environment variables — but their Git history told a different story. You can search for this pattern yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GitHub code search query (do not run on live repos you don't own)
language:JavaScript "jwt.sign" "secret" NOT ".env"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results: tens of thousands of public files. Many are tutorials. Many are not.&lt;/p&gt;

&lt;p&gt;Here's the dangerous reality of Git history: even after you delete a secret from your codebase, it still exists in every commit before the deletion. Anyone who cloned the repo before you pushed the fix still has it. Anyone with access to GitHub's search can find it.&lt;/p&gt;

&lt;p&gt;I found this specific pattern in a repo with 3,000+ stars — a &lt;code&gt;.env.example&lt;/code&gt; file that had been copy-pasted directly into &lt;code&gt;.env&lt;/code&gt; and committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env (committed by mistake, later deleted — but still in git log)&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mongodb://localhost/myapp
&lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;f7k2j9x1_my_production_secret_do_not_share
&lt;span class="nv"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk_live_...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sk_live_&lt;/code&gt; key had already been rotated. The JWT secret had not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; before your first commit. Always.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitignore&lt;/span&gt;
.env
.env.local
.env.production
&lt;span class="k"&gt;*&lt;/span&gt;.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a secret is already in your history, rotation is mandatory — not optional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rotate immediately, then use git filter-repo to purge history&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;git-filter-repo
git filter-repo &lt;span class="nt"&gt;--path&lt;/span&gt; .env &lt;span class="nt"&gt;--invert-paths&lt;/span&gt;

&lt;span class="c"&gt;# Force push to all remotes (coordinate with your team first)&lt;/span&gt;
git push origin &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then rotate the secret itself, invalidating all existing tokens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 4 — Expiration (&lt;code&gt;exp&lt;/code&gt;) Not Enforced
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frequency: 5 of 12 repos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Signature verification and expiry validation are separate concerns. Some repos verify one and skip the other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Subtly broken — verifies signature but issues tokens that never expire&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// No expiresIn option!&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// And on the verification side, a custom decoder that only checks signature:&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUserFromToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// decode, not verify&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jwt.decode()&lt;/code&gt; does not verify the signature. It does not check expiry. It literally just base64-decodes the payload. If your auth middleware calls &lt;code&gt;jwt.decode()&lt;/code&gt; instead of &lt;code&gt;jwt.verify()&lt;/code&gt;, your entire auth layer is bypassed.&lt;/p&gt;

&lt;p&gt;Even when &lt;code&gt;jwt.verify()&lt;/code&gt; is used correctly, I found several repos that issued tokens with no &lt;code&gt;exp&lt;/code&gt; claim at all. A token without &lt;code&gt;exp&lt;/code&gt; never expires. Ever. If it leaks — in a log file, in a frontend error boundary, in a network request captured at a coffee shop — it's valid forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Always set expiresIn&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// short-lived access tokens&lt;/span&gt;
      &lt;span class="na"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Always use verify(), never decode() for auth&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// clockTolerance: 30  (optional: allow 30s clock skew)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most applications: access tokens at 15 minutes, refresh tokens at 7 days stored in an httpOnly cookie.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 5 — Signing and Verification With Different Secrets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frequency: 3 of 12 repos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one is subtle and disproportionately common in microservice architectures. It surfaces as a maddening "invalid signature" error in production that works perfectly in development.&lt;/p&gt;

&lt;p&gt;Here's the pattern from a multi-service repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// auth-service/src/token.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// api-gateway/src/middleware/auth.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// different env var name!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;docker-compose.yml&lt;/code&gt; for local development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;auth-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev-secret"&lt;/span&gt;

&lt;span class="na"&gt;api-gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;APP_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev-secret"&lt;/span&gt; &lt;span class="c1"&gt;# same value locally, easy to miss&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production the values diverged. Someone set &lt;code&gt;APP_SECRET&lt;/code&gt; to a new rotated value and didn't update &lt;code&gt;JWT_SECRET&lt;/code&gt;. Tokens signed by auth-service became immediately invalid everywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Standardize the environment variable name across all services. Use a shared secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) so all services draw from a single source of truth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Centralized config with validation — same module or same env var name across services&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;jwtSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;jwtAlgorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Validate at startup, not at runtime&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jwtSecret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JWT_SECRET is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And document which service signs, which services verify, and what the shared secret name is. Put it in your runbook.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 6 — Short Human-Readable Secrets With HS256
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frequency: 9 of 12 repos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the most insidious mistake because the code looks "fixed." The developer has moved the secret to an environment variable. They're not hardcoding it. They're being responsible. But the secret itself is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp2024Secure!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve characters. Entirely alphabetic with one symbol and a number. Memorable enough to type by hand.&lt;/p&gt;

&lt;p&gt;Here's the entropy problem: HS256 operates on raw bytes. The question is not how long the string looks — it's how many possible values it could be.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Character set for typical "memorable" passwords:
- Lowercase letters: 26
- Uppercase letters: 26  
- Digits: 10
- Common symbols: 32
Total: ~94 characters

Entropy per character: log2(94) ≈ 6.5 bits
A 12-character password: 12 × 6.5 = ~78 bits of entropy

HS256 security requirement: 256 bits minimum
Shortfall: 178 bits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;78 bits sounds like a lot until you realize that modern GPU clusters can crack bcrypt hashes at millions of attempts per second. JWT secrets are not bcrypt — they're HMAC-SHA256, which is orders of magnitude faster to brute force. Security researchers maintain curated dictionaries of common JWT secrets — "MyApp2024", company names, and any string that appears in public repos are all in there.&lt;/p&gt;

&lt;p&gt;If your secret is human-generated, it's a target. Full stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use a CSPRNG (Cryptographically Secure Pseudorandom Number Generator) to generate 256+ bits of entropy. The result will not be memorable. That's the point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js — run this once, save the output in your secrets manager&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 32 bytes = 256 bits&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Example output: 3Hs8dKj2mNpQrT5wXvYzAb7eGcFhIiJl (never use this specific value)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Or from the shell&lt;/span&gt;
node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"console.log(require('crypto').randomBytes(32).toString('base64url'))"&lt;/span&gt;

&lt;span class="c"&gt;# Or with openssl&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result looks like noise. It should. Store it in your secrets manager, inject it at runtime, rotate it when team members leave or secrets are suspected of compromise.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pre-Ship JWT Checklist
&lt;/h2&gt;

&lt;p&gt;Before you deploy JWT-based auth, run through every item:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secret quality&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Secret is generated by a CSPRNG (not typed by a human)&lt;/li&gt;
&lt;li&gt;[ ] Secret is at least 32 bytes (256 bits) for HS256&lt;/li&gt;
&lt;li&gt;[ ] Secret is stored in a secrets manager or environment variable — never in code
&lt;strong&gt;Code hygiene&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;.env&lt;/code&gt; is in &lt;code&gt;.gitignore&lt;/code&gt; and was never committed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;git log&lt;/code&gt; does not contain the secret in any historical commit&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;jwt.verify()&lt;/code&gt; is used everywhere (never &lt;code&gt;jwt.decode()&lt;/code&gt; for auth)
&lt;strong&gt;Algorithm enforcement&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;algorithms&lt;/code&gt; option is explicitly set in &lt;code&gt;jwt.verify()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;"none"&lt;/code&gt; is not in the allowed algorithms list&lt;/li&gt;
&lt;li&gt;[ ] If using RS256/ES256, private key never leaves the signing service
&lt;strong&gt;Token lifecycle&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] All tokens have an &lt;code&gt;exp&lt;/code&gt; claim (&lt;code&gt;expiresIn&lt;/code&gt; option is set)&lt;/li&gt;
&lt;li&gt;[ ] Access token expiry is 15–60 minutes maximum&lt;/li&gt;
&lt;li&gt;[ ] Refresh tokens are stored in httpOnly cookies, not localStorage
&lt;strong&gt;Multi-service safety&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] All services use the same environment variable name for the JWT secret&lt;/li&gt;
&lt;li&gt;[ ] Secret is sourced from a shared secrets store, not per-service &lt;code&gt;.env&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;[ ] Startup validation throws if &lt;code&gt;JWT_SECRET&lt;/code&gt; is missing or too short&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;None of these mistakes are exotic. They're copy-paste errors, tutorial leftovers, and configuration drift between environments. The reason they appear in repos with thousands of stars is the same reason they appear everywhere: JWT's happy path is completely invisible to security issues. The token works. Auth passes. Tests pass. Nothing breaks until someone exploits it.&lt;/p&gt;

&lt;p&gt;The most important item on the checklist is the first one. A CSPRNG-generated secret neutralizes most of the other risks at source — you can't brute force 256 bits of true randomness, even with excellent tooling. Get that right, enforce algorithm allowlists, and set expiry. The rest is defense in depth.&lt;/p&gt;

&lt;p&gt;If you want to generate a properly random secret right now without writing a script, &lt;a href="https://jwtsecretgenerator.com" rel="noopener noreferrer"&gt;jwtsecretgenerator.com&lt;/a&gt; does it entirely client-side with the Web Crypto API. No accounts, no logs, no network request for the key itself. Just a cryptographically sound secret ready to paste into your secrets manager.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found a mistake I missed? A 7th pattern that deserves to be on this list? Drop it in the comments — I'll update the post and credit you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>jwt</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
