<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://zentsu.app/feed.xml" rel="self" type="application/atom+xml" /><link href="https://zentsu.app/" rel="alternate" type="text/html" /><updated>2026-07-19T01:30:41+00:00</updated><id>https://zentsu.app/feed.xml</id><title type="html">Zentsu</title><subtitle>Software for Apple platforms, built by Zentsu LLC.</subtitle><entry><title type="html">What every segment of a JWT actually means</title><link href="https://zentsu.app/blog/2026/jwt-anatomy/" rel="alternate" type="text/html" title="What every segment of a JWT actually means" /><published>2026-05-21T00:00:00+00:00</published><updated>2026-05-21T00:00:00+00:00</updated><id>https://zentsu.app/blog/2026/jwt-anatomy</id><content type="html" xml:base="https://zentsu.app/blog/2026/jwt-anatomy/"><![CDATA[<p>If you have ever shipped an authenticated API, you have pasted a JWT
into a debugger, watched it expand into three colored chunks, and moved
on. That habit is fine until the day a token is rejected for reasons
the colored chunks don’t explain, or the day someone on the security
team asks why you pasted a production token into a website.</p>

<p>We build dev tools at Zentsu, so we look at JWTs constantly. This is
the explanation we wish we had handed our past selves: what is actually
in those three segments, what the debugger glosses over, and where the
abstractions fray.</p>

<h2 id="the-shape">The shape</h2>

<p>A JWT is three Base64Url-encoded JSON-ish blobs joined by dots:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;header&gt;.&lt;payload&gt;.&lt;signature&gt;
</code></pre></div></div>

<p>That’s it. There is no fourth segment. The “encryption” you may have
heard about is JWE, a different and rarer spec; the standard JWT you
get from your auth provider is a JWS: <em>signed</em>, not encrypted. Anyone
who can read the bytes of the token can read the contents. That is the
single most-mispronounced fact about JWTs in production code.</p>

<h2 id="segment-1-the-header">Segment 1: the header</h2>

<p>The header is a tiny JSON object describing how the rest of the token
was produced. Two fields matter in practice:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">alg</code>: the signing algorithm. <code class="language-plaintext highlighter-rouge">HS256</code> (HMAC-SHA-256, symmetric
secret), <code class="language-plaintext highlighter-rouge">RS256</code> (RSA, asymmetric), <code class="language-plaintext highlighter-rouge">ES256</code> (ECDSA on P-256). If you
ever see <code class="language-plaintext highlighter-rouge">alg: "none"</code> on a token your code is about to trust, stop
reading and go fix that. The “none” algorithm is the canonical JWT
vulnerability and has been since 2015.</li>
  <li><code class="language-plaintext highlighter-rouge">kid</code>: a key ID. When your IdP rotates signing keys, the verifier
uses <code class="language-plaintext highlighter-rouge">kid</code> to pick the right public key from a JWKS endpoint. If you
are hard-coding a single public key in your service, key rotation
will eventually break you.</li>
</ul>

<p>There is no secret data here. It is metadata. The fields are
<em>advisory</em> unless your verification library specifically pins them.
A token can claim <code class="language-plaintext highlighter-rouge">alg: "RS256"</code> and still be a forgery if the verifier
doesn’t refuse mismatched algorithms.</p>

<h2 id="segment-2-the-payload-claims">Segment 2: the payload (claims)</h2>

<p>The payload is the part everyone actually cares about. It is also a
JSON object, and the keys are called <em>claims</em>. The standard ones are
short on purpose because every byte costs you in headers:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">iss</code>: issuer. Who minted this token. <code class="language-plaintext highlighter-rouge">https://auth.example.com</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">sub</code>: subject. Who the token is <em>about</em>. Usually a user ID. This
is not a username; treat it as opaque.</li>
  <li><code class="language-plaintext highlighter-rouge">aud</code>: audience. Who the token is <em>for</em>. If your service is not in
the audience list, you must reject the token even if the signature
is valid.</li>
  <li><code class="language-plaintext highlighter-rouge">exp</code>: expiration time, as seconds since the Unix epoch. Past this,
the token is dead.</li>
  <li><code class="language-plaintext highlighter-rouge">nbf</code>: “not before.” Tokens with <code class="language-plaintext highlighter-rouge">nbf</code> in the future are not yet
valid. Most issuers omit this; some compliance contexts require it.</li>
  <li><code class="language-plaintext highlighter-rouge">iat</code>: issued at. When the token was minted. Useful for sliding
expiry and audit logs.</li>
  <li><code class="language-plaintext highlighter-rouge">jti</code>: JWT ID. A unique identifier for this specific token, used
for revocation lists and replay-detection.</li>
</ul>

<p>Anything else in the payload is custom. Roles, tenant IDs, feature
flags. Your team can put whatever it wants here. Just remember: it is
all readable by anyone with the token, and every byte is sent on every
request. Custom claims are not a database; they are a lossy cache
optimized for “I trust this enough to skip a lookup.”</p>

<h2 id="segment-3-the-signature">Segment 3: the signature</h2>

<p>This is where most explanations get hand-wavy. The signature is <em>not</em>
a password. It is not a secret you compare against another secret. It
is a verifiability check.</p>

<p>What actually happens: the issuer computes a cryptographic signature
over <code class="language-plaintext highlighter-rouge">Base64Url(header) + "." + Base64Url(payload)</code>, using either a
shared secret (HMAC) or a private key (RSA/ECDSA). The signature gets
Base64Url-encoded and appended as the third segment.</p>

<p>When your service receives the token, it does the same computation
with the verifying key (the shared HMAC secret, or the issuer’s public
key) and checks that the result matches the third segment. If yes, the
header and payload have not been altered since they were signed. If
no, somebody tampered or you have the wrong key.</p>

<p>That’s the entire trust model. The signature does not encrypt anything.
It does not hide the payload. It only proves the header and payload
were not modified after signing. If you wanted secrecy, you wanted JWE
or you wanted to not put the data in a JWT at all.</p>

<h2 id="base64url-not-base64">Base64Url, not Base64</h2>

<p>The encoding is <em>Base64Url-safe without padding</em>. That means <code class="language-plaintext highlighter-rouge">+</code>
becomes <code class="language-plaintext highlighter-rouge">-</code>, <code class="language-plaintext highlighter-rouge">/</code> becomes <code class="language-plaintext highlighter-rouge">_</code>, and trailing <code class="language-plaintext highlighter-rouge">=</code> characters are
stripped. If your debugger pastes the token through a generic Base64
decoder, the last segment will fail half the time. Many debuggers
silently re-pad behind the scenes; the segments looking “clean” in the
UI does not mean the bytes round-trip cleanly through your <code class="language-plaintext highlighter-rouge">base64</code>
shell command.</p>

<h2 id="why-the-online-debugger-is-a-problem">Why the online debugger is a problem</h2>

<p>The token in your clipboard is, by definition, valid. Signature matches,
<code class="language-plaintext highlighter-rouge">exp</code> hasn’t passed, <code class="language-plaintext highlighter-rouge">aud</code> is your service. If it weren’t all of those,
you wouldn’t be debugging it.</p>

<p>When you paste it into an online JWT debugger, you have just sent a
working credential to a third party’s web server. The site’s privacy
policy may or may not say it’s stored. The browser’s history remembers.
Shell history, if you <code class="language-plaintext highlighter-rouge">curl</code>‘d it, remembers. Some IdPs issue twelve-
hour lifetimes: twelve hours during which a copy of the credential
lives somewhere you don’t control.</p>

<p>The fix is not “use a more reputable site.” The fix is to never let
production tokens leave the machine. Decode them locally, in a tool
that processes bytes in-memory and forgets: a small offline utility,
a CLI, anything that doesn’t phone home.</p>

<h2 id="what-to-actually-check-every-time">What to actually check, every time</h2>

<p>When a token shows up and you’re not sure why your service rejected it,
walk this list before reaching for a debugger:</p>

<ol>
  <li>Is <code class="language-plaintext highlighter-rouge">alg</code> what your verifier expects, <em>pinned by config</em>?</li>
  <li>Is <code class="language-plaintext highlighter-rouge">iss</code> the issuer you trust?</li>
  <li>Is <code class="language-plaintext highlighter-rouge">aud</code> your service?</li>
  <li>Is <code class="language-plaintext highlighter-rouge">exp</code> in the future, allowing small clock-skew tolerance?</li>
  <li>Is <code class="language-plaintext highlighter-rouge">nbf</code> absent or already in the past?</li>
  <li>Does the signature verify against the <em>current</em> signing key, after
honoring <code class="language-plaintext highlighter-rouge">kid</code>?</li>
</ol>

<p>Most production rejection bugs are one of those six. The rest are
clock skew between machines, key-rotation timing windows, and,
depressingly often, somebody hand-rolling a verifier that doesn’t
pin <code class="language-plaintext highlighter-rouge">alg</code>.</p>

<h2 id="the-takeaway">The takeaway</h2>

<p>A JWT is not magic. It is a signed, base64url-wrapped JSON pair where
the contents are public, the signature proves untampered transport, and
the standard claims tell you who minted it, who it is for, when it
became valid, and when it dies. Treat every token as a credential (because it is one)
and decode them where the bytes cannot leak.</p>

<p>If you spend any time in auth code, an offline JWT decoder belongs in
your toolbox next to your hex editor and your regex tester. Bench’s
<a href="/tools/jwt-decoder">JWT decoder</a> processes everything locally. Try
it the next time a token confuses you.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A technical walkthrough of JWT structure, claims, signature mechanics, and why online debuggers are a security problem.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://zentsu.app/assets/zentsu-og.png" /><media:content medium="image" url="https://zentsu.app/assets/zentsu-og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>