JWT anatomy: the three parts decoded
A JWT looks opaque until you understand its structure. The dots are delimiters, not noise.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header (base64url)
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ ← Payload (base64url)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature (base64url) Header — a JSON object describing the token type and algorithm. At minimum:
{ "alg": "HS256", "typ": "JWT" } Payload — a JSON object containing claims. The JWT spec registers standard claims; applications can add custom claims:
{
"sub": "1234567890", // subject (user ID)
"name": "John Doe", // custom claim
"iat": 1516239022, // issued at (Unix timestamp)
"exp": 1516325422 // expiry (Unix timestamp)
} Signature — HMAC-SHA256 of base64url(header) + "." + base64url(payload) using the issuer's secret (for HS256) or signed with the issuer's private key (for RS256/ES256). The signature is what makes the token tamper-evident. Modify one character of the payload and the signature no longer matches.
Base64url differs from standard base64 in two characters: + becomes - and / becomes _. Trailing = padding is omitted. This makes the token safe to embed in URLs and HTTP headers without percent-encoding.
Standard claims and what they mean
Decoding vs verifying: a critical distinction
Decoding reads the payload. Verifying proves the payload is authentic. They are not the same operation.
Decoding is trivial — any base64url decoder can do it. Verifying requires the issuer's secret or public key and a library that implements the correct algorithm. A decoded token that has not been verified is not trustworthy. An attacker can craft a valid-looking JWT with any claims they want and decode it successfully.
When is decode-only valid? Debugging. When you want to inspect what claims a token contains without needing to trust it — reading the sub for a log entry, checking the exp to see why a session is expiring, understanding what your auth provider is putting in the payload.
When is verification mandatory? Any time your application makes an authorization decision based on token claims. In production code, always use a battle-tested JWT library (jsonwebtoken for Node, PyJWT for Python, java-jwt for Java) and explicitly specify the expected algorithm. Never trust the alg claim from the token itself.
HS256 vs RS256 vs ES256 — choosing a signing algorithm
HS256 (HMAC-SHA256) — symmetric: one shared secret signs and verifies. Fast, simple, suitable for internal microservices where both issuer and verifier are controlled by you. The major risk: any service with the secret can issue tokens. If one service is compromised, all services accepting that secret are exposed.
RS256 (RSA-SHA256) — asymmetric: private key signs, public key verifies. The auth server holds the private key; all other services hold only the public key. A compromised API service cannot forge tokens. The public key is distributable — publish it at a JWKS endpoint (/.well-known/jwks.json) and other services can fetch it. The tradeoff: RSA operations are slower and the key is larger.
ES256 (ECDSA-SHA256) — asymmetric like RS256 but using elliptic curve cryptography. Same security properties as RS256 with significantly smaller keys and faster operations. The recommended choice for new systems. ES256 private keys are 32 bytes; RS256 private keys are typically 2048 bits (256 bytes). Supported by all modern JWT libraries.
JWT security pitfalls that appear in real CVEs
The alg:none attack (CVE-2015-9235 and similar) — described above. Fix: hardcode the expected algorithm in your verification call. Never read it from the token. Most libraries have a algorithms parameter for this purpose.
The RS256-to-HS256 confusion attack — if a library verifies using the algorithm from the token header, an attacker can switch the algorithm from RS256 to HS256 and sign the token with the server's public key (which is, by definition, public knowledge). The library then verifies an HMAC-SHA256 signature using the public key as the HMAC secret — and accepts it. Fix: same as above — specify the algorithm explicitly.
Missing exp validation — common in quick implementations. A token with a past exp is technically still cryptographically valid. If your library verifies the signature but does not check exp, expired tokens continue to work. Most production libraries check exp by default — confirm in your library's documentation.
No audience validation — a token issued for your internal API is valid for your payment service if both use the same signing key and neither validates aud. Set and validate the audience claim, especially in multi-service architectures.
Catch JWT errors in production before users do
Expired token errors, invalid signature exceptions, and missing claim panics show up in Sentry with full stack traces, user context, and request metadata. Configure Sentry's token scrubbing to redact JWTs from error payloads before they hit the dashboard.