JSON Web Tokens have become the default mechanism for authentication in modern web applications, particularly those with single-page frontends and API backends. Despite their widespread use, many developers treat JWTs as opaque strings — they copy library code, get authentication working, and move on without understanding what is inside the token or how the security model works. This leads to vulnerabilities. Understanding JWTs from the inside out helps you use them correctly and recognize when they are the wrong tool for the job.

The Three-Part Structure

A JWT is a string composed of three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The three parts are:

Each part is Base64url-encoded (not standard Base64 — the URL-safe variant that uses - and _ instead of + and /, with padding omitted). You can decode the header and payload with a simple Base64 decode. The signature is a binary value that only makes sense when verified against the signing key.

The Header

The header is a JSON object that typically contains two fields:

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

The alg field specifies the signing algorithm. The typ field is almost always "JWT". Some headers include a kid (key ID) field that tells the verifier which key to use, which is important in systems that rotate signing keys.

The Payload and Claims

The payload contains claims — statements about the user and additional metadata. Claims come in three categories:

Registered claims are predefined by the JWT specification (RFC 7519). They are optional but recommended:

Public claims are custom claims registered in the IANA JSON Web Token Claims registry to avoid naming conflicts. Private claims are custom claims agreed upon between parties, such as role, email, or permissions.

A typical payload looks like this:

{
  "sub": "user_8f3k2j",
  "name": "Jane Smith",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1775366400,
  "exp": 1775370000
}

Signing Algorithms

The signature is what makes JWTs trustworthy. Without it, anyone could create a token with any claims they wanted. The two most common algorithm families are:

HMAC (HS256, HS384, HS512): Symmetric algorithms that use a single shared secret. The same key signs and verifies the token. HS256 uses HMAC with SHA-256. This is simpler to set up but requires that every service that needs to verify tokens has access to the secret key.

HMACSHA256(
  base64urlEncode(header) + "." + base64urlEncode(payload),
  secret
)

RSA (RS256, RS384, RS512): Asymmetric algorithms that use a private/public key pair. The issuer signs with the private key, and anyone can verify with the public key. This is ideal for distributed systems where multiple services need to verify tokens but only the auth server should create them.

There is also ES256 (ECDSA with P-256), which provides equivalent security to RS256 with shorter keys and signatures. It is increasingly preferred for new systems.

Token Validation Flow

When a server receives a JWT, it must perform these validation steps in order:

  1. Parse the token into its three Base64url-encoded parts.
  2. Decode the header and determine the signing algorithm.
  3. Verify the signature using the appropriate key and algorithm. If the signature does not match, reject the token immediately.
  4. Decode the payload and check the registered claims:
    • Is exp in the future? If not, the token has expired.
    • Is nbf in the past? If not, the token is not yet valid.
    • Does iss match the expected issuer?
    • Does aud include this service?
  5. Extract the custom claims (user ID, roles, permissions) and proceed with the request.

Every single one of these steps matters. Skipping any of them opens a security hole.

Common Security Mistakes

JWTs are frequently misused. Here are the most dangerous mistakes developers make:

1. Not validating the signature. This is the most critical error. If you decode the payload without verifying the signature, an attacker can forge any token they want. Some developers mistakenly treat JWT decoding as authentication.

2. The "alg: none" attack. The JWT spec allows an "alg": "none" header, meaning no signature. If your server blindly trusts the alg field from the token, an attacker can set it to "none", remove the signature, and the token passes validation. Always enforce a whitelist of acceptable algorithms on the server side.

3. Algorithm confusion attacks. If a server is configured to accept RS256 (asymmetric), an attacker might craft a token with "alg": "HS256" and sign it using the public key as the HMAC secret. Since the public key is, well, public, the attacker has the "secret." The server sees HS256, uses the public key as the HMAC key, and the signature verifies. The fix: never let the token dictate which algorithm to use. Configure accepted algorithms on the server.

4. Storing sensitive data in the payload. The payload is Base64url-encoded, not encrypted. Anyone with the token can decode and read the claims. Never put passwords, credit card numbers, or other secrets in a JWT payload.

5. Not checking expiration. If you skip the exp check, tokens are valid forever once issued. A stolen token becomes a permanent skeleton key.

6. Using long expiration times. JWTs cannot be revoked once issued (without additional infrastructure). A token valid for 30 days means a compromised token is exploitable for 30 days. Keep access token lifetimes short (5-15 minutes) and use refresh tokens for longer sessions.

JWTs vs. Session Cookies

JWTs and server-side sessions solve the same problem — maintaining authentication state — but with different trade-offs:

When to Use JWTs

JWTs are a good fit when:

JWTs are a poor fit when:

JWTs are a powerful tool, but they are not a universal solution. Use them where their stateless, self-contained nature is genuinely valuable, and pair them with short lifetimes and rigorous validation. When in doubt, server-side sessions with secure cookies remain a simpler, safer default for most web applications.