<?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: Melissa</title>
    <description>The latest articles on DEV Community by Melissa (@melissa_50bf3b377e4a56cec).</description>
    <link>https://dev.to/melissa_50bf3b377e4a56cec</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4133094%2F4a46cc1c-405a-485d-a795-2ab9eb956b87.webp</url>
      <title>DEV Community: Melissa</title>
      <link>https://dev.to/melissa_50bf3b377e4a56cec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/melissa_50bf3b377e4a56cec"/>
    <language>en</language>
    <item>
      <title>Your JWT secret might literally be "secret". Here's how to check.</title>
      <dc:creator>Melissa</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:18:24 +0000</pubDate>
      <link>https://dev.to/melissa_50bf3b377e4a56cec/your-jwt-secret-might-literally-be-secret-heres-how-to-check-109f</link>
      <guid>https://dev.to/melissa_50bf3b377e4a56cec/your-jwt-secret-might-literally-be-secret-heres-how-to-check-109f</guid>
      <description>&lt;p&gt;If your app signs JWTs with HS256, there's a check worth doing today. It takes a minute, and the failure mode is bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a weak HMAC secret is worse than it sounds
&lt;/h2&gt;

&lt;p&gt;An HS256 signature is just &lt;code&gt;HMAC-SHA256(secret, header + "." + payload)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The header and payload are not secret. They're only base64url-encoded, so anyone holding one of your tokens has the exact message that was signed &lt;em&gt;and&lt;/em&gt; the resulting signature. The only unknown is the secret.&lt;/p&gt;

&lt;p&gt;That means someone can guess secrets &lt;strong&gt;offline&lt;/strong&gt;: take a candidate, compute the HMAC, compare it to the signature. No login endpoint, no rate limit, no lockout, no logs on your side. Tools like hashcat and jwt_tool already do this at scale.&lt;/p&gt;

&lt;p&gt;If they guess right, they can mint their own tokens: any &lt;code&gt;sub&lt;/code&gt;, any &lt;code&gt;role&lt;/code&gt;, any &lt;code&gt;exp&lt;/code&gt;. Your server will trust all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How weak secrets end up in production
&lt;/h2&gt;

&lt;p&gt;Nobody picks &lt;code&gt;secret&lt;/code&gt; on purpose. It gets there through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a tutorial that used &lt;code&gt;jwt.sign(payload, "secret")&lt;/code&gt; and was never revisited&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.example&lt;/code&gt; with &lt;code&gt;JWT_SECRET=changeme&lt;/code&gt; copied to &lt;code&gt;.env&lt;/code&gt; and shipped&lt;/li&gt;
&lt;li&gt;a placeholder from local dev that quietly became the prod value&lt;/li&gt;
&lt;li&gt;a short "memorable" secret, because someone had to type it into a dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checking a token yourself
&lt;/h2&gt;

&lt;p&gt;Here's the whole check in a few lines of browser or Node 20+ JavaScript:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trySecret&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;guess&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&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;sign&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;const&lt;/span&gt; &lt;span class="nx"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;b64url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;/g&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/=+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&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;b64url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &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;guess&lt;/span&gt; &lt;span class="k"&gt;of&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&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;changeme&lt;/span&gt;&lt;span class="dl"&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;jwt_secret&lt;/span&gt;&lt;span class="dl"&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;your-256-bit-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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;trySecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guess&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Weak secret:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guess&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;One gotcha I hit while building this: &lt;strong&gt;sign the original &lt;code&gt;header.payload&lt;/code&gt; substring exactly as it appears in the token.&lt;/strong&gt; If you decode the JSON and re-serialize it, key order or whitespace can change, the HMAC won't match, and the check will tell you a weak secret is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  While you're in there, check these too
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;alg: none&lt;/code&gt;&lt;/strong&gt;: a token claiming no signature should never be accepted. Make sure your library is pinned to the algorithms you expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;exp&lt;/code&gt;&lt;/strong&gt;: a token that never expires is valid forever once it leaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal data in the payload&lt;/strong&gt;: base64 is not encryption. Emails, names, and internal IDs in the payload are readable by anyone holding the token.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Use a long random secret, not a word:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32
&lt;span class="c"&gt;# or&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('base64'))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then rotate it, which invalidates every existing token. If several services need to verify tokens, consider RS256 or ES256 instead, so verifiers only ever hold a public key.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tool for this
&lt;/h2&gt;

&lt;p&gt;I got tired of writing that loop, so I built the check into a JWT debugger: &lt;a href="https://devbelt.dev/jwt-debugger" rel="noopener noreferrer"&gt;devbelt.dev/jwt-debugger&lt;/a&gt;. It tests the token against a small built-in list of common weak secrets in a Web Worker, flags &lt;code&gt;alg: none&lt;/code&gt;, missing &lt;code&gt;exp&lt;/code&gt;, and likely personal data, and runs entirely in your browser. Nothing is sent anywhere, and nothing goes into the URL.&lt;/p&gt;

&lt;p&gt;To be clear about what it is: a self-test for tokens you own, with a deliberately tiny wordlist. It's not a cracking tool. If you'd rather not paste a production token into any website, mine included, the snippet above does the same core check locally.&lt;/p&gt;

&lt;p&gt;It also explains &lt;em&gt;why&lt;/em&gt; verification fails (wrong &lt;code&gt;aud&lt;/code&gt;, wrong &lt;code&gt;iss&lt;/code&gt;, clock skew) instead of only saying "invalid signature", which is the part I use most.&lt;/p&gt;

&lt;p&gt;What's the worst JWT setup you've run into in the wild? I'm curious which mistakes are most common.&lt;/p&gt;

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