Β· By DevToolHub Team

JWT Tokens Explained: Structure, Validation, Pitfalls

JSON Web Tokens (JWTs) are everywhere in modern web development. If you have ever implemented authentication for a REST API, configured OAuth 2.0, or worked with single sign-on (SSO), you have encountered JWTs. They are the standard mechanism for stateless authentication in web applications and microservices. Yet many developers treat them as magic strings without understanding what is inside β€” and that leads to security mistakes. This guide breaks down the JWT structure (header, payload, signature), explains how to decode and validate tokens, covers the most common security pitfalls (including the none algorithm attack and localStorage vulnerabilities), and shows when JWTs are the right choice vs. server-side sessions. To quickly inspect any JWT, use our JWT Decoder β€” it runs entirely in your browser.

The Three Parts

Every JWT consists of three Base64URL-encoded sections separated by dots:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
└──────── Header β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€ Payload β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Signature β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Header β€” metadata about the token:

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

Payload β€” the actual data (called β€œclaims”):

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1716239022
}

Signature β€” cryptographic proof that the token hasn’t been tampered with. Created by signing the header + payload with a secret key:

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

Standard Claims

The JWT specification (RFC 7519) defines several registered claim names:

ClaimNamePurpose
subSubjectWho the token is about (usually user ID)
issIssuerWho created the token
audAudienceWho the token is intended for
expExpirationWhen the token expires (Unix timestamp)
iatIssued AtWhen the token was created
nbfNot BeforeToken is not valid before this time
jtiJWT IDUnique identifier for the token

You can also add any custom claims you need, like role, email, or permissions.

Encoding vs. Encryption

This is the most common misconception about JWTs: the payload is encoded, not encrypted. Anyone can decode a JWT and read its contents β€” no secret key needed. Base64URL is an encoding scheme, not a security mechanism.

This means you should never put sensitive data in a JWT payload:

  • Passwords or password hashes
  • Credit card numbers or financial data
  • Private API keys or secrets
  • Full social security numbers or government IDs
  • Medical records or other regulated PII

The signature only guarantees integrity (the token hasn’t been modified), not confidentiality (the contents are hidden).

If you need encrypted tokens, look into JWE (JSON Web Encryption), which encrypts the payload so it cannot be read without the decryption key. However, JWE is significantly more complex and rarely needed β€” the better approach is simply not putting sensitive data in the token.

Decoding JWTs in Code

JavaScript / Node.js

// Decode without verification (for inspection only)
function decodeJwt(token) {
  const [header, payload] = token.split('.').slice(0, 2)
    .map(part => JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/'))));
  return { header, payload };
}

// Verify with jsonwebtoken (Node.js)
const jwt = require('jsonwebtoken');
try {
  const decoded = jwt.verify(token, secretOrPublicKey);
  console.log(decoded.sub); // user ID
} catch (err) {
  console.error('Invalid token:', err.message);
}

Python

import jwt  # pip install PyJWT

# Decode without verification (inspection only)
payload = jwt.decode(token, options={"verify_signature": False})

# Decode with verification
try:
    payload = jwt.decode(token, secret_key, algorithms=["HS256"])
    print(payload["sub"])
except jwt.ExpiredSignatureError:
    print("Token expired")
except jwt.InvalidTokenError:
    print("Invalid token")

Go

// Using github.com/golang-jwt/jwt/v5
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
    if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
    }
    return []byte(secretKey), nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
    fmt.Println(claims["sub"])
}

Common Security Pitfalls

Not validating the signature. Decoding a JWT is easy. Verifying its signature requires the secret key or public key. Always verify server-side before trusting any claims.

Using the none algorithm. Some libraries accept "alg": "none" which means no signature at all. Always reject tokens with alg: none in production. Explicitly whitelist allowed algorithms:

// Good: explicitly specify allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });

// Bad: trusting whatever algorithm the token claims
jwt.decode(token); // never trust unverified tokens

Not checking expiration. Always validate the exp claim. An expired token should be rejected, period.

Storing JWTs in localStorage. This makes them vulnerable to XSS attacks. Any JavaScript running on your page (including injected scripts from third-party dependencies) can read localStorage. Prefer httpOnly cookies for browser-based applications.

Using overly long expiration times. JWTs cannot be revoked once issued (unless you maintain a blacklist). Short-lived tokens (15-60 minutes) with refresh tokens are the standard pattern.

Algorithm confusion attacks. An attacker might change the header to "alg": "HS256" and sign with the RS256 public key (which is often publicly available). Libraries that don’t enforce the expected algorithm are vulnerable. Always specify the expected algorithm when verifying.

HS256 vs RS256

The two most common signing algorithms:

HS256 (HMAC + SHA-256) β€” symmetric. The same secret key signs and verifies. Simple, but both parties need the secret.

Auth Server ──(secret)──> signs token
API Server  ──(secret)──> verifies token

RS256 (RSA + SHA-256) β€” asymmetric. A private key signs, a public key verifies. Better for distributed systems where multiple services need to verify tokens but only one should create them.

Auth Server ──(private key)──> signs token
API Server  ──(public key)───> verifies token (cannot forge tokens)
HS256RS256
Key typeShared secretPublic/private key pair
Key distributionSecret must be shared (risky)Only public key shared (safe)
PerformanceFasterSlower (RSA operations)
Best forSingle-server appsMicroservices, third-party verification

When to Use JWTs

Good use cases:

  • Stateless API authentication (the most common use)
  • Single sign-on (SSO) across multiple services
  • Short-lived authorization tokens in OAuth 2.0 flows
  • Service-to-service communication in microservices
  • One-time-use tokens (email verification, password reset)

Consider alternatives when:

  • You need to revoke tokens instantly (use server-side sessions)
  • You’re building a simple single-server app (session cookies are simpler)
  • You need to store large amounts of data (JWTs add overhead to every request)
  • You need confidentiality (JWTs are transparent β€” consider JWE or opaque tokens)

The Access Token + Refresh Token Pattern

The standard approach for JWT-based authentication:

  1. User logs in β†’ server issues an access token (short-lived, 15 min) and a refresh token (long-lived, 7 days)
  2. Client sends the access token with every API request
  3. When the access token expires, the client sends the refresh token to get a new access token
  4. If the refresh token is expired or revoked, the user must log in again

This balances security (short-lived access tokens limit damage from theft) with usability (users don’t have to log in every 15 minutes).

Try It Yourself

Use our JWT Decoder to paste any JWT and instantly see its decoded header, payload, and expiration status β€” all processed in your browser. Your tokens never leave your device.

For understanding the Base64 encoding used in JWTs, see our Base64 encoder and decoder, and learn how Base64 encoding works.

To understand the hash algorithms used in JWT signatures, check out our Hash Generator and the guide on MD5 vs SHA-256.

Further Reading

FAQ

Can anyone read the contents of a JWT?
Yes. The header and payload of a JWT are only Base64URL-encoded, not encrypted. Anyone can decode them without any key. The signature only ensures the token hasn't been tampered with β€” it does not hide the contents. Never put passwords, credit card numbers, or other secrets in a JWT payload.
What happens when a JWT expires?
The token becomes invalid. The server should reject it by checking the exp (expiration) claim. However, JWTs are stateless β€” the server doesn't automatically know a token has expired unless it actively validates the exp claim on every request. Always validate expiration server-side.
Can I revoke a JWT before it expires?
Not directly β€” JWTs are stateless by design. Once issued, they're valid until they expire. To revoke tokens early, you need a server-side blacklist (a database of revoked token IDs). This adds statefulness but is necessary for logout, password changes, or compromised tokens. Alternatively, use short-lived tokens (15 minutes) with refresh tokens.
Should I store JWTs in localStorage or cookies?
Prefer httpOnly cookies. localStorage is accessible to any JavaScript on the page, making it vulnerable to XSS attacks β€” if an attacker injects script, they can steal the token. httpOnly cookies cannot be read by JavaScript and are automatically sent with requests, but require CSRF protection.
What is the difference between HS256 and RS256?
HS256 (HMAC-SHA256) is symmetric β€” the same secret key signs and verifies the token. Both parties must share the secret. RS256 (RSA-SHA256) is asymmetric β€” a private key signs the token and a public key verifies it. RS256 is better for distributed systems where many services verify tokens but only the auth server creates them.
How long should a JWT access token live?
The industry standard is 15-60 minutes for access tokens. Short-lived tokens limit the damage if a token is stolen β€” an attacker only has a narrow window. Pair short-lived access tokens with longer-lived refresh tokens (7-30 days) stored in httpOnly cookies. The refresh token is used to get new access tokens without requiring the user to log in again.
What is the maximum size of a JWT?
There is no hard limit in the JWT specification, but practical limits exist. HTTP headers (where JWTs are usually sent) have size limits: most servers default to 8 KB. A typical JWT with standard claims is 200-500 bytes. Adding many custom claims can push it to 1-2 KB. If your JWT exceeds 4 KB, consider moving data to a server-side session and keeping only essential claims in the token.
jwt authentication security api web

Related Tools

Related Articles