<?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: Mind vs Drive</title>
    <description>The latest articles on DEV Community by Mind vs Drive (@mind_vsdrive_30e41cb9738).</description>
    <link>https://dev.to/mind_vsdrive_30e41cb9738</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%2F4225697%2F4dfbce6e-b439-4635-af55-16aedbba1102.png</url>
      <title>DEV Community: Mind vs Drive</title>
      <link>https://dev.to/mind_vsdrive_30e41cb9738</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mind_vsdrive_30e41cb9738"/>
    <language>en</language>
    <item>
      <title>How to Decode a JWT (and What You Can and Can't Learn From It)</title>
      <dc:creator>Mind vs Drive</dc:creator>
      <pubDate>Wed, 04 Nov 2026 10:54:36 +0000</pubDate>
      <link>https://dev.to/mind_vsdrive_30e41cb9738/how-to-decode-a-jwt-and-what-you-can-and-cant-learn-from-it-1nk2</link>
      <guid>https://dev.to/mind_vsdrive_30e41cb9738/how-to-decode-a-jwt-and-what-you-can-and-cant-learn-from-it-1nk2</guid>
      <description>&lt;p&gt;If you've built anything with login in the last few years, you've handled a JSON Web Token. It shows up in an &lt;code&gt;Authorization: Bearer ...&lt;/code&gt; header, in a cookie, or in a response from your auth provider, and it looks like a long random string. It isn't random at all. Most of it is plain JSON that anyone can read in a few seconds, and knowing how to read it saves a lot of debugging time.&lt;/p&gt;

&lt;p&gt;This post walks through what a JWT is made of, decodes one by hand, and covers the mistakes I see most often. At the end I mention a free decoder I built for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three parts of a JWT
&lt;/h2&gt;

&lt;p&gt;A JWT is three pieces joined by dots:&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header&lt;/strong&gt;: a small JSON object saying which algorithm signed the token (for example &lt;code&gt;HS256&lt;/code&gt; or &lt;code&gt;RS256&lt;/code&gt;) and the token type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt;: a JSON object of &lt;em&gt;claims&lt;/em&gt;, which are facts about the user and the token, such as a user id, a role, when it was issued and when it expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature&lt;/strong&gt;: a cryptographic signature over the header and payload, made with a secret or private key, so the server can tell whether anyone changed them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The header and payload are not encrypted. They're encoded with &lt;strong&gt;Base64URL&lt;/strong&gt;, a variant of Base64 that swaps &lt;code&gt;+&lt;/code&gt; for &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; for &lt;code&gt;_&lt;/code&gt; and drops the &lt;code&gt;=&lt;/code&gt; padding so the result is safe in URLs and headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;Here's a token I made for this post (the signature part is a placeholder):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDI0IiwibmFtZSI6IkFzaGEiLCJyb2xlIjoiZWRpdG9yIiwiaWF0IjoxNzkzNzc5MjAwLCJleHAiOjE3OTM3ODI4MDB9.SIGNATURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Split it on the dots and decode the first part from Base64URL:&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="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"HS256"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decode the second part:&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="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1024"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Asha"&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;"editor"&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;1793779200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1793782800&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;Now the claims mean something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt; is the subject, usually the user id: &lt;code&gt;1024&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role&lt;/code&gt; is a custom claim the app added: &lt;code&gt;editor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iat&lt;/code&gt; (issued at) is a Unix timestamp: 1793779200 is 4 Nov 2026, 08:00 UTC.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exp&lt;/code&gt; (expires) is 1793782800, exactly 3,600 seconds later, so the token lives for one hour.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can do the decoding yourself in the browser console:&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;part&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="s1"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&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;/-/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;/_/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&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;atob&lt;/code&gt; expects standard Base64, which is why the &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; get swapped back first. It's usually fine with missing padding, but some decoders aren't, so add &lt;code&gt;=&lt;/code&gt; until the length is a multiple of 4 if you hit an error.&lt;/p&gt;

&lt;p&gt;Or in a terminal:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eyJzdWIiOiIxMDI0IiwibmFtZSI6IkFzaGEiLCJyb2xlIjoiZWRpdG9yIiwiaWF0IjoxNzkzNzc5MjAwLCJleHAiOjE3OTM3ODI4MDB9'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'_-'&lt;/span&gt; &lt;span class="s1"&gt;'/+'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;base64 -d&lt;/code&gt; may complain about the missing padding at the end, but it still prints the JSON.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding is not verifying
&lt;/h2&gt;

&lt;p&gt;This is the most important thing to understand. Decoding tells you what the token &lt;em&gt;says&lt;/em&gt;. Only verifying the signature with the right key tells you whether to &lt;em&gt;believe&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;Anyone can make a token with &lt;code&gt;"role":"admin"&lt;/code&gt; in the payload. If your server reads the payload without checking the signature, you've built an authentication bypass. Always verify with your library (&lt;code&gt;jsonwebtoken&lt;/code&gt;, &lt;code&gt;jose&lt;/code&gt;, PyJWT and so on), with the algorithm pinned on the server side rather than taken from the token header.&lt;/p&gt;

&lt;p&gt;That's also why an online decoder will show you the header and payload but warn that the signature can't be checked. Without your secret or public key, no tool can check it, and you shouldn't paste a production secret into a website anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes worth catching
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Putting secrets in the payload.&lt;/strong&gt; Because the payload is only encoded, an email address, an internal id, or worse, a password or API key in there is readable by anyone who sees the token, including browser extensions and log files. Keep payloads to what the client is allowed to know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Trusting &lt;code&gt;alg: none&lt;/code&gt;.&lt;/strong&gt; A header with &lt;code&gt;"alg":"none"&lt;/code&gt; means the token isn't signed. Old libraries sometimes accepted these. Reject them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Confusing seconds and milliseconds.&lt;/strong&gt; JWT timestamps are in &lt;em&gt;seconds&lt;/em&gt;. JavaScript's &lt;code&gt;Date.now()&lt;/code&gt; returns &lt;em&gt;milliseconds&lt;/em&gt;. If you compare &lt;code&gt;exp&lt;/code&gt; to &lt;code&gt;Date.now()&lt;/code&gt; directly, every token looks expired for the next 50,000 years, or never expires, depending on which way you get it wrong. Use &lt;code&gt;Date.now() / 1000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Clock skew.&lt;/strong&gt; If a token is "expired" right after login, check the server clocks. Most libraries allow a small leeway (30 to 60 seconds) for exactly this reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Huge tokens.&lt;/strong&gt; Every claim you add travels with every request. A token with a long permissions list can push headers past proxy limits (often 8 KB). If your token is several kilobytes, move data to the server and look it up by &lt;code&gt;sub&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to decode a JWT
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debugging a 401: check whether &lt;code&gt;exp&lt;/code&gt; has passed, whether &lt;code&gt;aud&lt;/code&gt; matches your API, and whether &lt;code&gt;iss&lt;/code&gt; is the issuer you expect.&lt;/li&gt;
&lt;li&gt;Integrating a new auth provider: see which claims it actually sends before writing code that depends on them.&lt;/li&gt;
&lt;li&gt;Reviewing security: make sure nothing sensitive is sitting in the payload.&lt;/li&gt;
&lt;li&gt;Learning: seeing real tokens makes the spec much easier to follow.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I got tired of writing the same console snippet, so I built a free JWT decoder at &lt;a href="https://base64decodetool.com/jwt/" rel="noopener noreferrer"&gt;base64decodetool.com/jwt&lt;/a&gt;. You paste a token and it shows the header, payload and standard claims (with &lt;code&gt;exp&lt;/code&gt; and &lt;code&gt;iat&lt;/code&gt; turned into readable dates), flags &lt;code&gt;alg: none&lt;/code&gt; and expired tokens, and runs entirely in your browser, so the token isn't uploaded anywhere. The same site has plain Base64 and Base64URL encoders and decoders if you need to look at one part on its own.&lt;/p&gt;

&lt;p&gt;Whatever tool you use, remember the rule: decode to understand, verify to trust.&lt;/p&gt;

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