Skip to main content
{/}codefunc
Web & APIJuly 3, 2026 · 7 min read

JWT Anatomy: Header, Payload, and Signature Explained

A deep dive into JSON Web Token structure, signing algorithms, common vulnerabilities, and a safe workflow for decoding tokens in development without exposing production secrets.

By codefunc

jwtsecurityauthenticationapioauth

JSON Web Tokens (JWTs) power authentication for countless APIs, SPAs, and microservices. They travel in Authorization: Bearer headers, hide inside cookies, and show up in every OAuth 2.0 / OpenID Connect flow. They look like opaque secrets — but JWTs are signed, not encrypted, by default. Anyone holding the string can Base64url-decode the header and payload. This guide explains exactly what is inside, how verification works, where teams get hurt, and how to inspect tokens safely during development.

Bottom line: Decoding reveals claims; only cryptographic verification proves authenticity. Never store sensitive data in a JWT payload. Use client-side decoders for real tokens, never untrusted upload services.

The three-part structure

A JWT is three Base64url-encoded segments joined by dots:

xxxxx.yyyyy.zzzzz
  │      │      └── Signature (verify integrity)
  │      └── Payload (claims)
  └── Header (metadata)

Real example (signature truncated for display):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Segment Contains Encrypted?
Header Algorithm, type, optional kid No
Payload Claims (user id, roles, expiry) No
Signature HMAC or asymmetric signature N/A — it is the proof

Decoding ≠ verification. A forged token decodes to JSON just fine. Your API must verify the signature on every request.

Header: algorithms and key hints

Decoded header example:

{
  "alg": "HS256",
  "typ": "JWT"
}
Field Meaning
alg Signing algorithm — HS256, RS256, ES256, etc.
typ Token type — almost always JWT
kid Key ID — tells verifier which public key to use (JWKS rotation)

Algorithm families

Family Example Secret material Typical deployment
HMAC HS256, HS384, HS512 Shared symmetric secret Monolith, BFF, small services
RSA RS256, RS384, RS512 Private sign / public verify Multi-service, public JWKS endpoint
ECDSA ES256, ES384, ES512 Elliptic curve key pair Mobile, high-performance APIs

The alg: none vulnerability

Attackers have historically exploited libraries that accepted {"alg":"none"} and skipped verification. Production APIs must reject unsigned tokens and use an allowlist of permitted algorithms. Never let the token header dictate verification behavior blindly.

Payload: registered, public, and private claims

Decoded payload example:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "iss": "https://auth.example.com",
  "aud": "api.example.com",
  "scope": "read:orders write:orders"
}

Registered claims (RFC 7519)

Claim Name Purpose
iss Issuer Who minted the token
sub Subject User or entity id
aud Audience Intended recipient service
exp Expiration Unix timestamp — reject after
nbf Not before Reject before this time
iat Issued at Mint time
jti JWT ID Unique token id for replay prevention

Custom claims

Teams add roles, permissions, tenant_id, plan, or email. Anything in the payload is visible to the client if the token is stored in the browser. Do not put passwords, credit card numbers, or PII you would not show in DevTools.

Signature: how verification actually works

HMAC (HS256)

signature = HMACSHA256(
  base64url(header) + "." + base64url(payload),
  shared_secret
)

Verifier recomputes the HMAC with the same secret. Match → authentic. Mismatch → reject.

Risk: every service that verifies must hold the secret. Leak in one microservice compromises all.

RSA / ECDSA (RS256, ES256)

signature = Sign(
  base64url(header) + "." + base64url(payload),
  private_key
)

Verifier checks with public key (often published at /.well-known/jwks.json). Private key stays on the auth server only.

Benefit: edge APIs verify without shared secrets.

JWS vs JWE: signed vs encrypted

Standard What it does Payload visible without key?
JWS (signed) Integrity + authenticity Yes — anyone can decode
JWE (encrypted) Confidentiality No — needs decryption key

Most API Bearer tokens are JWS. If you need hiding (e.g. sensitive claims in a public channel), use JWE or do not put data in the token.

Safe decode workflow for developers

  1. Copy token from DevTools → Network → request headers, or from your app's storage (carefully).
  2. Open a client-side decoder — processing must stay in the browser.
  3. Inspect exp first — expired tokens cause unexplained 401s.
  4. Check iss and aud match your environment (dev vs staging vs prod).
  5. Review roles / scope for authorization bugs.
  6. Do not share decoded tokens in tickets, Loom videos, or public gists.

What decode tells you

  • User id (sub)
  • Whether token is expired
  • Granted scopes or roles
  • Which auth server issued it

What decode cannot tell you

  • Whether signature is valid (need secret or public key)
  • Whether user was banned after issuance
  • Whether refresh token is still valid

Debugging the "random 401" checklist

Check Question
exp Is token expired? Clock skew between servers?
nbf Is token not yet valid?
iss Wrong auth server / environment mix-up?
aud API rejecting wrong audience?
Algorithm API expects RS256 but token is HS256?
Key rotation kid points to retired key?
Revocation User logged out but access token still unexpired?

Short-lived access tokens (5–15 minutes) plus refresh flows reduce the window for stale tokens.

JWT vs session cookies

Aspect JWT (stateless) Session cookie
Server storage None required at verifier Session store required
Revocation Hard without blocklist Easy — delete session
Size Large (claims in every request) Small cookie id
Horizontal scale Simple verification Needs shared session store
XSS risk if in localStorage High — JS can read token HttpOnly cookies resist JS

Hybrid pattern (common): short-lived JWT access token + refresh token in HttpOnly cookie + server-side revocation table for refresh tokens.

Common vulnerabilities and mistakes

1. Storing JWTs in localStorage

Any XSS attack exfiltrates the token. Prefer HttpOnly, Secure, SameSite cookies for browser sessions when your architecture allows.

2. Trusting the payload without verification

Middleware must verify signature before reading claims. Never branch on unverified JSON.

3. Long-lived access tokens

A 30-day access token is a 30-day breach window. Short access + refresh is industry standard.

4. Sensitive data in claims

Emails and names in JWTs end up in logs, analytics, and crash reports attached to request headers.

5. Algorithm confusion

Forcing HS256 verification with a public key as the HMAC secret has caused real breaches. Use explicit algorithm allowlists in your JWT library.

OAuth 2.0 and OpenID Connect context

In OIDC, the ID Token is a JWT proving authentication event (who logged in, when, at which issuer). The Access Token may or may not be a JWT depending on authorization server.

When debugging login:

  • ID Token → identity claims (sub, email, name)
  • Access Token → API authorization (scope, aud for resource server)

Decode each separately; they serve different purposes.

HS256 vs RS256: decision guide

Choose HS256 when:

  • Single backend signs and verifies
  • Few services, shared secret rotation is manageable

Choose RS256 when:

  • Multiple APIs verify tokens independently
  • You publish JWKS for key rotation
  • Third parties verify without sharing your signing secret

Use these codefunc tools alongside this guide:

Practical takeaway

JWTs are readable signed envelopes. Decode in the browser for debugging — verify on the server for security. Check expiry and audience before blaming application bugs. Treat custom claims as public. Use short lifetimes, prefer asymmetric signing at scale, and never paste production tokens into server-side decoders you do not trust.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool