JSON Web Tokens (JWTs) are the most common format for stateless authentication on the modern web. They look like a string of random characters, but they are a structured, three-part encoding: a header describing the algorithm, a payload containing the claims, and a signature that proves the token was issued by a trusted party. Once you understand the three parts, every JWT you encounter becomes readable.
The Anatomy of a JWT
A JWT is three Base64URL-encoded strings connected by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Each part has a role:
- Header — declares the algorithm used to sign the token (
HS256,RS256, etc.) and the token type (JWT). The decoded header is{"alg":"HS256","typ":"JWT"}. - Payload — the claims. This is arbitrary JSON; standardized claim names cover most common fields. The decoded payload is
{"sub":"1234567890","name":"John Doe"}. - Signature — the cryptographic proof that the token was issued by a trusted party. For HS256, this is the HMAC-SHA256 of the first two parts using a shared secret:
HMAC-SHA256(secret, header + "." + payload).
The full token is base64url-encoded because the resulting string is URL-safe (no +, /, or padding) and can travel in HTTP headers, URL parameters, cookies, or local storage without escaping. The JWT Decoder splits the token and shows each part in plain JSON.
Base64URL vs. Base64
JWTs use Base64URL, a small variant of standard Base64. The two substitutions are:
+(in standard Base64) becomes-(in Base64URL)/(in standard Base64) becomes_(in Base64URL)
Padding characters (=) are stripped entirely. The resulting string contains only ASCII letters, digits, hyphens, and underscores — all of which are allowed in URL paths, query parameters, and HTTP header values without escaping.
The motivation is convenience. A JWT can be embedded in a URL like https://example.com/api?token=eyJ... without any percent-encoding. It can be placed in a header like Authorization: Bearer eyJ... without breaking the header grammar. It can be stored in a cookie without escaping the value. The cost is a small amount of conversion code; the benefit is universal portability.
Standard Claims
JWT claims are arbitrary JSON, but RFC 7519 defines a set of registered claims that are common enough to be standardized. They are recommended but optional:
- iss (issuer) — the URI of the entity that issued the token. Useful in distributed systems where multiple services can issue tokens.
- sub (subject) — the principal the token is about, typically a user ID or an account identifier.
- aud (audience) — the intended recipient(s). The token should be rejected if the audience does not match the verifying service.
- exp (expiration time) — a Unix timestamp after which the token must be rejected. The most important claim; tokens without an
expare valid forever. - nbf (not before) — a Unix timestamp before which the token must be rejected. Useful for delaying the validity of a token until a specific time.
- iat (issued at) — a Unix timestamp when the token was issued. Useful for determining the age of a token.
- jti (JWT ID) — a unique identifier for the token. Used to prevent replay attacks by requiring each token to be used only once.
Custom claims are also allowed and common. A token issued by an e-commerce site might contain {"role": "customer", "tier": "gold", "cart_id": "abc123"}; the application code reads these claims to make authorization decisions.
How Signing Works
The signature is the part that makes a JWT trustworthy. Without it, anyone could construct a token claiming to be the CEO of the company. With it, the receiving service can verify that the token was issued by a trusted party and not tampered with.
For HMAC algorithms (HS256, HS384, HS512), the signature is computed as:
signature = HMAC-SHA256(secret, base64url(header) + "." + base64url(payload))
The receiving service performs the same computation with the same secret and compares the result to the signature in the token. If they match, the token is valid. If they differ, either the token was tampered with (a different header or payload) or the token was not issued by the expected party (different secret).
For RSA algorithms (RS256, RS384, RS512), the issuer signs with a private RSA key; the verifier checks with the corresponding public key. The asymmetric model allows the issuer to be a separate entity from the verifier (a typical identity provider pattern) without sharing the signing key.
For ECDSA algorithms (ES256, ES384, ES512), the issuer signs with an ECDSA private key; the verifier checks with the public key. The signatures are smaller than RSA for the same security level.
For EdDSA (Ed25519), the issuer signs with an Ed25519 private key. EdDSA is increasingly common in new systems because it is fast, has small signatures, and is resistant to implementation errors.
Verification
Verifying a JWT in application code follows a standard pattern. The verifier:
- Splits the token into three parts.
- Base64URL-decodes the header and payload.
- Validates the header claims (algorithm is the expected one, type is JWT).
- Reconstructs the signing input:
base64url(header) + "." + base64url(payload). - Computes the signature using the expected algorithm and the verifier's secret or public key.
- Compares the computed signature to the signature in the token. A mismatch means the token is invalid.
- Validates the standard claims:
expis in the future,nbfis in the past,audmatches the verifier. - Returns the claims to the application code for authorization decisions.
Every major language has a JWT library that implements this flow. The libraries are well-tested and handle edge cases (timing attacks on signature comparison, clock skew on exp, malformed UTF-8 in claims). Writing verification code from scratch is a security antipattern; use a library.
JWT vs. Session Cookies
JWTs are not the only way to maintain authenticated state. The two main alternatives are JWTs and server-side session cookies.
Server-side session cookies store a random session ID in the cookie and keep the session state on the server. The server looks up the session ID on each request to find the user. The trade-off is stateful: the server must keep session state in memory or a database.
JWTs are stateless: the token itself contains the user information. The server validates the signature and reads the claims directly. The trade-off is that revocation is harder — once a token is issued, it is valid until exp, even if the user logs out. (Solutions: short-lived tokens with refresh tokens, revocation lists, or a centralized session store that mirrors JWT contents.)
The choice depends on the application. Server-side sessions are the right default for browser-based applications where the server controls the cookie path and lifetime. JWTs are the right default for API authentication, especially in service-to-service communication where the issuer and verifier are different services with no shared session store.
Where JWTs Are Used
JWTs appear in many places across modern web infrastructure:
- OAuth 2.0 access tokens — when a user logs into a third-party application via “Sign in with Google”, the resulting access token is a JWT. The application sends the token to the resource server (Google APIs); the resource server validates the signature and grants access.
- OpenID Connect ID tokens — the identity layer on top of OAuth 2.0. The ID token is a JWT containing the user’s identity claims.
- Single Sign-On (SSO) — tokens issued by an identity provider and validated by multiple relying applications. The asymmetric signing model (RS256/ES256) is essential here; the same private key signs for all services, but each service only needs the public key to verify.
- API authentication — machine-to-machine calls often use JWTs instead of API keys for the granularity (claims-based authorization) and the time-bound validity.
- Email magic links — a JWT containing a short-lived token is included in the email link; clicking the link sends the token to the server, which verifies the signature and grants one-time access.
Common Pitfalls
JWTs are not without their hazards. The recurring mistakes:
- alg: none — accepting tokens with the algorithm set to
noneallows anyone to forge tokens. Many early JWT libraries had this vulnerability. Always reject thenonealgorithm explicitly. - Algorithm confusion — a service that expects RS256 (verified with a public key) but accepts HS256 (verified with the public key as the HMAC secret) is vulnerable. The attacker submits an HS256 token signed with the public key — which is, by definition, public. Always pin the expected algorithm in the verifier.
- Missing expiration — tokens without an
expclaim are valid forever. If the token is compromised, the attacker has permanent access. Always includeexp. - Putting sensitive data in the payload — the payload is base64-encoded, not encrypted. Anyone with the token can read the claims. JWTs are not a confidential channel.
- XSS and storage — JWTs stored in localStorage are vulnerable to XSS attacks. The safer pattern is to store in an HttpOnly cookie that the JavaScript cannot access.
- Long-lived tokens — tokens with a one-year expiration are convenient but dangerous. The shorter the validity, the smaller the blast radius of a compromise. 15 minutes to 1 hour is the standard for access tokens, paired with refresh tokens for long-lived sessions.
JWT in the Browser
The JWT format is widely used in the browser, but with caveats. The dominant pattern:
- The user authenticates with a username and password.
- The server returns an access token (JWT, short-lived) and a refresh token (random string, longer-lived).
- The browser stores the tokens in memory or in an HttpOnly cookie.
- Each API request includes the access token in the
Authorizationheader. - When the access token expires, the browser sends the refresh token to the auth server to get a new access token.
- The refresh token is rotated on each use to limit the blast radius of a leak.
The JWT Decoder is useful for debugging this flow: paste a token from the browser’s network tab or the server’s logs and inspect the claims. Common debug questions:
- “Why is my token rejected?” — check the
exp; if it is in the past, the token has expired. - “Why is the audience wrong?” — check the
audclaim; it must match the verifier’s expected audience. - “Why is the algorithm unexpected?” — check the
algin the header; the verifier may be rejecting other algorithms as a security measure.
The Bottom Line
JWTs are a compact, signed, payload-bearing token format. The three parts are header, payload, and signature; the signature is the part that makes the token trustworthy. The most common algorithms are HS256 (symmetric, for issuers and verifiers that share a secret) and RS256 / ES256 (asymmetric, for identity-provider-style issuance). Always include exp, always pin the expected algorithm, and always use a tested library instead of writing verification from scratch. The JWT Decoder is the right tool for inspecting tokens during debugging; the verification itself belongs in your application code.
Further Reading
- RFC 7519 — JSON Web Token (JWT), the formal specification.
- RFC 7515 — JSON Web Signature (JWS), the signing layer that JWT uses.
- RFC 7517 — JSON Web Key (JWK), the format for representing public keys.
- OWASP JSON Web Token Cheat Sheet — the practical security guide for JWTs, with the algorithm-confusion attack pattern.
- Auth0 JWT Handbook — a comprehensive introduction to JWTs in modern applications.
Frequently Asked Questions
What is the difference between JWT and JWS? JWT is the token format; JWS is the signing layer. Every JWT is technically a JWS (a signed JSON structure), but JWS is also used directly for non-JSON payloads. The two are intertwined in practice.
Can JWT be encrypted? Yes — the JWE format (RFC 7516) encrypts the payload. JWE is much rarer than JWT in practice because most applications keep the payload unencrypted and rely on transport encryption (HTTPS) for confidentiality.
How are JWTs different from API keys? API keys are opaque random strings with a single claim (the API key IS the claim). JWTs are structured tokens with multiple claims encoded in the payload. The trade-off: API keys are simpler but cannot carry metadata; JWTs are more complex but can carry roles, scopes, expiration, and other claims.
Should I store JWTs in localStorage? No. localStorage is accessible to any JavaScript on the page, including malicious code injected via XSS. The safer pattern is to store in an HttpOnly cookie that the JavaScript cannot read (the browser sends it automatically with each request), or in memory (lost on page refresh, requiring a refresh token).
Why is the signature sometimes empty? The alg: none case. The token is not signed; anyone who knows the format can forge it. This is a security failure, not a feature. Always reject alg: none in the verifier.