<?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: Norris Chen</title>
    <description>The latest articles on DEV Community by Norris Chen (@tomynorth).</description>
    <link>https://dev.to/tomynorth</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%2F4042235%2Fcb35f352-c581-4598-8c70-f9fab3e0d90a.png</url>
      <title>DEV Community: Norris Chen</title>
      <link>https://dev.to/tomynorth</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tomynorth"/>
    <language>en</language>
    <item>
      <title>JWT Malformed, Invalid Signature, or Expired? A Practical Debugging Checklist</title>
      <dc:creator>Norris Chen</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:03:31 +0000</pubDate>
      <link>https://dev.to/tomynorth/jwt-malformed-invalid-signature-or-expired-a-practical-debugging-checklist-18eh</link>
      <guid>https://dev.to/tomynorth/jwt-malformed-invalid-signature-or-expired-a-practical-debugging-checklist-18eh</guid>
      <description>&lt;p&gt;JWT errors often look specific, but the message is only the starting point. The fastest way to debug them is to separate &lt;strong&gt;token shape&lt;/strong&gt;, &lt;strong&gt;signature verification&lt;/strong&gt;, and &lt;strong&gt;claim validation&lt;/strong&gt; instead of changing keys or expiration settings at random.&lt;/p&gt;

&lt;p&gt;Here is the checklist I use.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with the token shape
&lt;/h2&gt;

&lt;p&gt;A compact JWT normally contains three Base64URL-encoded segments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;header.payload.signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your library reports &lt;code&gt;jwt malformed&lt;/code&gt;, check the input before checking cryptography:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove the &lt;code&gt;Bearer&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;Make sure the value contains exactly two dots.&lt;/li&gt;
&lt;li&gt;Check for quotes, whitespace, line breaks, or a truncated environment variable.&lt;/li&gt;
&lt;li&gt;Confirm you did not pass a refresh token or an opaque session token to a JWT verifier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick JavaScript check:&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authorizationHeader&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;/^Bearer&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/i&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="nf"&gt;trim&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;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&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;!==&lt;/span&gt; &lt;span class="mi"&gt;3&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;Expected a compact JWT with three segments&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;Do not log production tokens while debugging. A JWT payload is encoded, not encrypted, and may contain user information.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Decode before you verify, but do not trust decoded data
&lt;/h2&gt;

&lt;p&gt;Decoding is useful for inspecting &lt;code&gt;alg&lt;/code&gt;, &lt;code&gt;kid&lt;/code&gt;, &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;, and &lt;code&gt;nbf&lt;/code&gt;. It does &lt;strong&gt;not&lt;/strong&gt; prove that the token came from your issuer.&lt;/p&gt;

&lt;p&gt;Use decoded values as clues only. Authorization decisions must happen after signature and claim verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. For &lt;code&gt;invalid signature&lt;/code&gt;, compare the verification contract
&lt;/h2&gt;

&lt;p&gt;This error usually means the verifier and issuer disagree about one of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm&lt;/strong&gt;: HS256 uses a shared secret; RS256/ES256 use a public key for verification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key&lt;/strong&gt;: development and production credentials may be different.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key ID&lt;/strong&gt;: with JWKS, the token's &lt;code&gt;kid&lt;/code&gt; must match a currently published key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token bytes&lt;/strong&gt;: copying, URL handling, or storage may have changed the token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret encoding&lt;/strong&gt;: one service may treat a value as plain text while another expects Base64-decoded bytes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not fix this by disabling signature verification or accepting every algorithm. Explicitly allow only the algorithms your issuer uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. For &lt;code&gt;jwt expired&lt;/code&gt;, inspect Unix time
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;exp&lt;/code&gt; claim is measured in &lt;strong&gt;seconds&lt;/strong&gt; since the Unix epoch, while JavaScript's &lt;code&gt;Date.now()&lt;/code&gt; returns milliseconds.&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&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="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Refresh through the trusted auth flow or require sign-in.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An expired access token should normally be refreshed through your authentication flow. Changing &lt;code&gt;exp&lt;/code&gt; inside the payload does not create a valid token because the signature will no longer match.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Check audience, issuer, and not-before separately
&lt;/h2&gt;

&lt;p&gt;A valid signature is not enough.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;iss&lt;/code&gt; should identify the expected token issuer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt; should include your API or application.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nbf&lt;/code&gt; means the token must not be accepted before that timestamp.&lt;/li&gt;
&lt;li&gt;Small clock differences can be handled with a narrow tolerance, but a large tolerance hides configuration problems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep expected issuer and audience values in configuration, and verify them explicitly in the backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reliable debugging order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the raw value has JWT shape.&lt;/li&gt;
&lt;li&gt;Decode the header and payload locally.&lt;/li&gt;
&lt;li&gt;Identify the algorithm and key source.&lt;/li&gt;
&lt;li&gt;Verify the signature with an explicit algorithm allowlist.&lt;/li&gt;
&lt;li&gt;Validate &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;nbf&lt;/code&gt;, &lt;code&gt;iss&lt;/code&gt;, and &lt;code&gt;aud&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Compare development and production configuration.&lt;/li&gt;
&lt;li&gt;Reproduce with a newly issued token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I turned this checklist into a browser-based reference covering the common messages, including malformed tokens, invalid signatures, expiration, audience, issuer, and not-before failures:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.easyjwt.top/jwt-error-decoder" rel="noopener noreferrer"&gt;https://www.easyjwt.top/jwt-error-decoder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The accompanying decoder runs locally in the browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.easyjwt.top/" rel="noopener noreferrer"&gt;https://www.easyjwt.top/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What JWT error has taken you the longest to diagnose? I would like to add more real failure cases to the guide.&lt;/p&gt;

</description>
      <category>jwt</category>
      <category>javascript</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
