HandyTools Hub

← All guides

JWT Explained: The Three-Part Structure, What Signing Proves, and the Classic Mistakes

2026-08-06

Inspect the request headers of any app with a login flow and you’ll almost certainly find something like Authorization: Bearer eyJhbGciOi... — that’s a JWT (JSON Web Token). It has become the default credential format for decoupled frontends and APIs: the server issues it once, the client sends it on every request, and the server can confirm “who you are” without touching a database. But JWTs are also a hotbed of misconceptions. People stuff secrets into them as if they were encrypted containers, or decode them and trust the contents without checking the signature — both are excellent ways to ship a security incident. This guide walks through the structure, what signing actually guarantees, and the classic traps.

The Three Parts: Header.Payload.Signature

A JWT always consists of three segments joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsImlhdCI6MTc1NDQ2NzIwMCwiZXhwIjoxNzU0NDcwODAwfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Part one is the header, which declares the token type and the signing algorithm. Decoded, it’s just JSON:

{"alg": "HS256", "typ": "JWT"}

Part two is the payload, carrying the actual “claims” — user ID, issued-at time, expiry, and so on:

{"sub": "12345", "iat": 1754467200, "exp": 1754470800}

Part three is the signature over the first two parts. For HS256, the signer computes HMAC-SHA256 over header + "." + payload using a secret key and appends the result. The signature exists to detect tampering: change a single character in the payload and signature verification fails.

All three segments are Base64URL-encoded, not plain Base64. Base64URL swaps the + and / characters of standard Base64 for - and _, and drops the trailing = padding — that makes the token safe to carry in URLs and headers. If you want to see the encoding rules in action, re-encode a decoded JSON fragment with the Base64 Encoder/Decoder and compare. In day-to-day work, the first move with any mystery token is pasting it into the JWT Decoder, which splits the three parts and pretty-prints the JSON instantly.

Signing Proves Integrity, Not Confidentiality

This is the single most important thing to internalize about JWTs: a signature is not encryption. The payload is merely Base64URL-encoded JSON. Anyone holding the token can decode it — no key required. Encoding changes the representation, not the readability; turning eyJzdWIiOiIxMjM0NSJ9 back into {"sub":"12345"} is a party trick, not a privilege.

What the signature actually guarantees is integrity and authenticity:

  • The contents have not been modified since issuance (any tampering breaks signature verification).
  • The token was really issued by whoever holds the signing key (assuming the key hasn’t leaked).

So the correct mental model is: a JWT is a tamper-evident envelope with transparent walls. User IDs, roles, permission scopes, and expiry times belong inside. Passwords, phone numbers, ID numbers, and secrets absolutely do not. If the contents genuinely need confidentiality, either keep them out of the token entirely or use JWE (encrypted tokens) — don’t rely on “nobody can read this gibberish.” The gibberish decodes in milliseconds.

HS256 vs RS256: Symmetric and Asymmetric

The alg field in the header names the signing algorithm. Two dominate in practice.

HS256 is HMAC-SHA256, a symmetric algorithm: the same shared secret is used to sign and to verify. The issuer computes an HMAC with the secret; the verifier recomputes it with the same secret and compares. It’s simple and fast, and it’s a fine choice for a single self-contained system where only your own backend ever sees the key. The catch: anyone who can verify a token can also forge one, so every party holding the key is a potential leak. If you want to see HMAC in action, run some text through the Hash Generator as HMAC-SHA256 — change the key and the output changes completely.

RS256 is RSA, an asymmetric algorithm: the issuer signs with a private key, and verifiers check the signature with the corresponding public key. The public key can be distributed freely — many identity providers publish theirs at /.well-known/jwks.json — so any service can verify tokens, while only the holder of the private key can mint new ones. This is a big win in multi-service architectures: gateways and third parties verify with the public key, and the private key never leaves the issuer.

The rule of thumb is straightforward: one system, key never leaves home — HS256; multiple parties verifying, or tokens crossing trust boundaries — RS256.

Common Claims: iss, sub, exp, iat

The JWT spec (RFC 7519) reserves a handful of standard claims, all living in the payload:

{
  "iss": "https://auth.example.com",
  "sub": "user-12345",
  "iat": 1754467200,
  "exp": 1754470800
}
  • iss (issuer): who issued the token, typically an identifier or URL for the auth service. Verifiers use it to confirm the token came from a service they trust.
  • sub (subject): who the token represents — usually a user ID.
  • iat (issued at): when the token was issued, as a seconds-level Unix timestamp.
  • exp (expiration time): when the token stops being valid, also a seconds-level timestamp. Once the current time passes exp, verifiers must reject the token. In the example above the token lives for exactly one hour.

These names are reserved — put custom data (roles, display names, feature flags) under your own keys rather than repurposing the standard ones. Two more worth knowing: nbf (not before), which delays validity until a given time, and aud (audience), which names the service a token is meant for — handy when tokens flow between microservices.

Four Classic Mistakes

1. Putting secrets in the payload. As covered above, the payload is world-readable. Password hashes, card numbers, and “internal notes” have all ended up in JWTs and then in breach reports. Before writing a field, ask: would I be comfortable printing this on a billboard?

2. Decoding without verifying. Decoding is not verification. Anyone can Base64URL-decode a token; trusting the payload without checking the signature is like trusting a note with no watermark — an attacker can simply mint their own token with sub: admin. The correct order of operations is: verify the signature → check alg is the algorithm you expect → validate exp/nbf/iss/aud → and only then read the payload. The JWT Decoder is for looking at contents; production code must verify signatures.

3. The alg=none saga. The JWT spec permits alg: "none" — an unsigned token for cases where security is guaranteed by another channel. Early versions of several JWT libraries accepted none by default, so attackers could rewrite the header to {"alg":"none"}, strip the signature, and forge anything. A related attack downgraded RS256 tokens to HS256 and used the public key (which is public!) as the HMAC secret. The aftermath is now baked into every serious library: you must explicitly pin the accepted algorithms instead of trusting whatever the token declares. Treat that as a hard rule.

4. Long-lived tokens with no revocation path. JWT’s statelessness is a double-edged sword: the server stores nothing after issuance, which means revoking a token early is genuinely hard. A leaked access token valid for 30 days buys an attacker 30 days. The standard mitigation is short-lived access tokens (minutes to hours) paired with long-lived refresh tokens that live server-side and can be revoked; high-security systems add a blacklist or a per-user token version. Never set exp to a year just to avoid refresh calls.

Debugging Expired and Invalid Tokens

When production hands you a 401 Invalid Token, this sequence finds the cause fastest:

  1. Decode and inspect. Paste the token into the JWT Decoder and confirm all three segments exist and the payload is valid JSON. A token with two segments or no dots was likely truncated or copied incompletely.
  2. Check exp. Compare the seconds-level timestamp against the current time. Expiry is the most common cause of 401s — especially with a skewed client clock or an aggressively short server-side lifetime.
  3. Check the header’s alg. If the server expects RS256 and you’re holding an HS256 token, verification fails every time. This bites constantly when switching between environments with different issuer configs.
  4. Suspect encoding issues. Tokens double-encoded in a URL, or Base64/Base64URL confusion (a - misread as a minus sign, say), can mangle the signature segment. The Base64 Encoder/Decoder lets you manually decode a segment to check whether the content survived intact.
  5. Content looks fine but verification still fails? Check that the secret or public key actually matches the issuer — a misloaded environment variable is the usual culprit.

Worth repeating: “decoding” in the steps above only inspects the contents and needs no key. Real signature verification must happen on a backend you trust, with the correct key.

Wrapping Up

JWT succeeded because it turns a trust problem into pure cryptography: verify the signature, trust the contents. Keep four things in mind — all three segments are Base64URL-encoded plaintext; the signature guarantees integrity, not secrecy; HS256 shares one secret while RS256 splits private and public keys; and verification must pin the algorithm and check exp. Internalize those, and you’ll sidestep ninety percent of the trouble JWTs cause.