JWT Tokens Explained: Complete Guide to JSON Web Tokens

March 11, 2026 20 min read Suvom Das

Table of Contents

Introduction to JWTs

JSON Web Tokens (JWTs) have become the de facto standard for authentication and authorization in modern web applications. Whether you are building a single-page application, a mobile app, a microservices architecture, or a serverless API, chances are you will encounter JWTs at some point in your development journey. Understanding how they work is essential for building secure, scalable systems.

A JWT is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity-protected with a Message Authentication Code (MAC) and optionally encrypted.

The JWT specification is defined in RFC 7519, published by the Internet Engineering Task Force (IETF) in May 2015. Since then, JWTs have been adopted by virtually every major platform, framework, and identity provider including Auth0, Okta, AWS Cognito, Firebase Authentication, Azure Active Directory, and Google Identity Platform.

In this comprehensive guide, we will break down every aspect of JSON Web Tokens: their three-part structure, the header and its algorithm field, the payload and its registered claims, how signatures work across different algorithms, how JWTs compare to traditional session-based authentication, popular JWT libraries, security best practices, common mistakes to avoid, and how to decode JWTs programmatically in Python and JavaScript.

JWT Structure (Header.Payload.Signature)

Every JWT is composed of exactly three parts, separated by dots (.). These three parts are the Header, Payload, and Signature. Each part is individually Base64URL-encoded, and the complete token looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

If you split this token on the dots, you get three Base64URL-encoded strings:

  1. Header (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9) -- Contains metadata about the token, including the signing algorithm and token type.
  2. Payload (eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ) -- Contains the claims, which are statements about the user and any additional data.
  3. Signature (SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c) -- The cryptographic signature used to verify the token has not been tampered with.

The Base64URL encoding is similar to standard Base64 but uses a URL-safe alphabet: + is replaced with -, / is replaced with _, and padding characters (=) are omitted. This makes JWTs safe to include in URLs, HTTP headers, and HTML forms without additional encoding.

It is critical to understand that Base64URL encoding is not encryption. Anyone who has a JWT can decode the header and payload and read their contents. The security of a JWT comes from the signature, which guarantees that the token has not been modified -- not from hiding its contents.

JWT Header

The JWT header is a JSON object that contains metadata about the token. At minimum, it contains the alg (algorithm) field, which specifies the cryptographic algorithm used to sign the token. It typically also includes the typ (type) field, which is set to "JWT".

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

Here are the most commonly used values for the alg field:

HMAC Algorithms (Symmetric)

RSA Algorithms (Asymmetric)

ECDSA Algorithms (Asymmetric)

RSA-PSS Algorithms (Asymmetric)

Edwards-Curve Algorithm

The "none" Algorithm

The header may also contain additional fields such as kid (Key ID), which identifies which key was used to sign the token when the issuer uses multiple keys, and jku (JWK Set URL), which points to a set of JSON Web Keys used for signature verification.

JWT Payload and Claims

The payload is the second part of a JWT and contains the claims. Claims are statements about an entity (typically the user) and additional data. There are three types of claims: registered claims, public claims, and private claims.

Registered Claims

Registered claims are a set of predefined claims defined in the JWT specification (RFC 7519). They are not mandatory but are recommended to provide a set of useful, interoperable claims. Here are all seven registered claims:

Public Claims

Public claims are custom claims that are registered in the IANA JSON Web Token Claims registry or defined using a collision-resistant name (such as a URI). Examples of public claims include email, name, picture, and email_verified, which are defined by the OpenID Connect specification. Using registered public claims ensures that your claim names do not collide with claims from other systems.

Private Claims

Private claims are custom claims created by agreement between the parties that use them. They are neither registered in the IANA registry nor defined as public claims. Examples include roles, permissions, org_id, tenant, or any other application-specific data. When using private claims, care should be taken to avoid collisions with registered or public claim names.

Here is an example payload with a mix of registered and private claims:

{
  "iss": "https://auth.example.com",
  "sub": "user_12345",
  "aud": "my-api",
  "exp": 1700003600,
  "iat": 1700000000,
  "jti": "unique-token-id-789",
  "name": "John Doe",
  "email": "[email protected]",
  "roles": ["admin", "editor"],
  "permissions": ["read:articles", "write:articles", "delete:articles"]
}

JWT Signature

The signature is the third and final part of a JWT. It is what makes JWTs trustworthy -- without a valid signature, any claims in the token could have been fabricated or modified by an attacker. The signature is created by taking the encoded header, adding a dot, adding the encoded payload, and then signing that string using the algorithm specified in the header.

For example, with HMAC SHA-256 (HS256), the signature is computed as:

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

HMAC vs RSA vs ECDSA

HMAC (HS256, HS384, HS512) uses symmetric cryptography. The same secret key is used to both create and verify the signature. This means that both the token issuer and the token consumer must possess the secret. HMAC is fast, simple, and appropriate for systems where the issuer and verifier are the same entity or share a trusted secret. However, if the verifier has the secret, it can also create valid tokens, which can be a security concern in distributed systems.

RSA (RS256, RS384, RS512, PS256, PS384, PS512) uses asymmetric cryptography. The token is signed with a private key and verified with the corresponding public key. This means that only the issuer needs the private key, while verifiers only need the public key. This is ideal for microservices architectures where many services need to verify tokens but should not be able to create them. The public key can be shared freely -- even published at a JWKS endpoint. RSA signatures are larger than HMAC or ECDSA signatures, and RSA signing is slower, but verification is fast.

ECDSA (ES256, ES384, ES512) also uses asymmetric cryptography but with elliptic curve mathematics. ECDSA provides the same security as RSA with significantly smaller key and signature sizes. An ES256 signature is about 64 bytes compared to 256 bytes for RS256. ECDSA is increasingly favored for performance-sensitive and bandwidth-constrained applications. The trade-off is that ECDSA signing can be slightly slower than RSA signing on some platforms, though verification is typically faster.

EdDSA is a newer signature scheme based on Edwards curves (typically Ed25519). It offers excellent performance, deterministic signatures (no random number generator needed during signing), and strong security with small key sizes. EdDSA is gaining rapid adoption and is supported by most modern JWT libraries.

JWT vs Session-Based Authentication

Before JWTs became popular, most web applications used session-based authentication. Understanding the differences helps you choose the right approach for your application.

Session-Based Authentication

In session-based authentication, when a user logs in, the server creates a session record in a database (or in-memory store like Redis) and sends the session ID to the client as a cookie. On each subsequent request, the client sends the session ID cookie, and the server looks up the session in its store to identify the user. The server is stateful -- it must maintain session state.

JWT-Based Authentication

In JWT-based authentication, when a user logs in, the server creates a JWT containing user information and signs it, then sends it to the client. The client stores the JWT (typically in memory, an HTTP-only cookie, or local storage) and includes it in the Authorization header of subsequent requests. The server verifies the JWT signature and reads the claims directly -- no database lookup is needed. The server is stateless.

Comparison

Aspect Sessions JWTs
State Stateful (server stores sessions) Stateless (token contains all info)
Scalability Requires shared session store for multiple servers Any server can verify the token independently
Revocation Easy -- delete the session Hard -- requires deny lists or short expiry
Cross-domain Difficult (cookies are domain-bound) Easy (token can be sent in any header)
Mobile support Cookie handling can be tricky Natural fit for mobile and SPAs
Size Small session ID cookie Larger token (can be 1KB+)

In practice, many modern systems use a hybrid approach: short-lived JWTs for authentication (5-15 minutes) paired with longer-lived refresh tokens stored server-side. This combines the scalability benefits of JWTs with the revocability of server-side sessions.

Common JWT Libraries

You should never implement JWT creation or verification from scratch. Instead, use a well-tested, actively maintained library for your language and platform. Here are the most popular JWT libraries:

Node.js: jsonwebtoken

The jsonwebtoken package is the most popular JWT library for Node.js with over 17 million weekly downloads on npm. It supports creating, signing, and verifying JWTs with HMAC, RSA, and ECDSA algorithms.

const jwt = require('jsonwebtoken');

// Sign a token
const token = jwt.sign({ sub: 'user_123', role: 'admin' }, 'your-secret', { expiresIn: '1h' });

// Verify a token
const decoded = jwt.verify(token, 'your-secret');
console.log(decoded.sub); // 'user_123'

Python: PyJWT

PyJWT is the most widely used Python JWT library. It supports all standard algorithms and provides a clean API for encoding and decoding JWTs.

import jwt
import datetime

# Encode a token
payload = {
    'sub': 'user_123',
    'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
    'iat': datetime.datetime.utcnow()
}
token = jwt.encode(payload, 'your-secret', algorithm='HS256')

# Decode and verify
decoded = jwt.decode(token, 'your-secret', algorithms=['HS256'])
print(decoded['sub'])  # 'user_123'

Java: java-jwt (Auth0)

The java-jwt library from Auth0 provides a fluent API for creating and verifying JWTs in Java applications.

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

// Create a token
Algorithm algorithm = Algorithm.HMAC256("your-secret");
String token = JWT.create()
    .withSubject("user_123")
    .withClaim("role", "admin")
    .withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
    .sign(algorithm);

// Verify a token
DecodedJWT decoded = JWT.require(algorithm).build().verify(token);
String subject = decoded.getSubject(); // "user_123"

Go: golang-jwt

The golang-jwt package (formerly dgrijalva/jwt-go) is the standard JWT library for Go.

import "github.com/golang-jwt/jwt/v5"

// Create a token
claims := jwt.MapClaims{
    "sub": "user_123",
    "exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, _ := token.SignedString([]byte("your-secret"))

// Verify a token
parsed, _ := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
    return []byte("your-secret"), nil
})

Other Languages

JWT Security Best Practices

JWTs are a powerful tool, but they can introduce serious security vulnerabilities if not used correctly. Follow these best practices to keep your JWT implementation secure:

1. Always Set an Expiration Time

Every JWT should have an exp claim with a reasonable expiration time. For access tokens, 5-15 minutes is a common range. For refresh tokens, 7-30 days is typical. Never issue tokens without an expiration -- a leaked token without expiry grants permanent access to the attacker.

2. Use Strong, Rotatable Signing Keys

For HMAC algorithms, use a cryptographically random secret that is at least 256 bits (32 bytes) long. For RSA, use at least 2048-bit keys (4096-bit preferred). Implement key rotation so that you can periodically change signing keys without invalidating all existing tokens. Use the kid (Key ID) header parameter to identify which key was used to sign a token, and publish your public keys at a JWKS endpoint for automated key discovery.

3. Always Transmit Over HTTPS

JWTs should only be transmitted over HTTPS. Since JWTs are not encrypted (by default), anyone who intercepts the token can decode and use it. HTTPS ensures the token cannot be intercepted in transit. Additionally, if you store tokens in cookies, set the Secure flag to prevent transmission over HTTP.

4. Do Not Store Sensitive Data in the Payload

Remember that the JWT payload is only Base64URL-encoded, not encrypted. Anyone with the token can decode and read the payload. Never include passwords, credit card numbers, social security numbers, or other sensitive personally identifiable information. If you need encrypted tokens, use JWE (JSON Web Encryption) instead of or in addition to JWS.

5. Always Validate the Token

When consuming a JWT, always validate: (a) the signature using the correct key and algorithm, (b) the exp claim to reject expired tokens, (c) the iss claim to ensure the token was issued by a trusted authority, (d) the aud claim to ensure the token is intended for your service, and (e) the nbf claim to reject tokens that are not yet valid.

6. Validate the Algorithm

When verifying a JWT, always specify the expected algorithm(s) and reject tokens that use a different algorithm. Never let the JWT header's alg field dictate which algorithm to use for verification. This prevents algorithm confusion attacks where an attacker changes the algorithm from RS256 to HS256 and signs with the public key (which the verifier possesses).

7. Reject the "none" Algorithm

Your JWT verification code should explicitly reject tokens with "alg": "none" unless you have a very specific reason to accept unsigned tokens (and you almost certainly do not). The "none" algorithm means the token has no signature and no integrity protection.

8. Implement Token Revocation

Since JWTs are stateless, you cannot simply "invalidate" a token. Implement a revocation strategy: use short-lived access tokens (so compromised tokens expire quickly), maintain a server-side deny list of revoked token IDs (jti), or use a combination of short-lived JWTs and longer-lived opaque refresh tokens that can be revoked in a database.

9. Consider Token Size

Every claim you add to a JWT increases its size, and the token is sent with every HTTP request. Keep your payloads lean -- include only the claims you need. A typical JWT should be under 1KB. If you find yourself packing large amounts of data into a JWT, consider storing that data server-side and including only a reference identifier in the token.

Common JWT Mistakes

Even experienced developers make mistakes with JWTs. Here are the most common pitfalls and how to avoid them:

1. Storing JWTs in localStorage

Storing JWTs in localStorage makes them accessible to any JavaScript running on the page, including scripts injected through XSS (Cross-Site Scripting) attacks. If an attacker can inject a single line of JavaScript, they can steal the JWT and gain full access to the user's account. Instead, store JWTs in HTTP-only, Secure cookies with the SameSite attribute set to Strict or Lax. If you must use in-browser storage (e.g., for a SPA that calls a different-origin API), store the token in memory (a JavaScript variable) and use short expiry times with a refresh token rotation strategy.

2. Not Validating the Signature

Some developers decode JWTs and use the claims without ever verifying the signature. This is extremely dangerous because an attacker can craft a JWT with any claims they want and send it to your API. Always verify the signature using a trusted library and the correct secret or public key before trusting any claims in the token.

3. Using the "none" Algorithm

The "none" algorithm was defined in the JWT specification for use cases where the JWT integrity is ensured by other means. However, if your server accepts JWTs with "alg": "none", an attacker can create unsigned tokens with arbitrary claims and your server will accept them. Always explicitly reject the "none" algorithm in your verification logic.

4. Algorithm Confusion Attacks

If your server is configured to verify tokens with RS256 (asymmetric), an attacker might change the alg header to HS256 (symmetric) and sign the token using the RSA public key as the HMAC secret. Since the server has the public key and uses it for verification, it would incorrectly verify the attacker's token. Prevent this by always specifying the expected algorithm when verifying, and never letting the token's header determine the verification algorithm.

5. Setting Excessively Long Expiration Times

Setting a JWT expiration time of days, weeks, or months defeats the purpose of short-lived tokens. If a token with a 30-day expiry is stolen, the attacker has 30 days of access. Use short-lived access tokens (5-15 minutes) and longer-lived refresh tokens with proper revocation support.

6. Not Checking the Audience Claim

If your system has multiple services and they all accept JWTs from the same issuer, failing to check the aud claim means a token intended for Service A can be used to access Service B. Always validate that the aud claim matches your service's expected audience value.

7. Including Sensitive Data

As mentioned earlier, JWTs are not encrypted. Including passwords, API keys, PII, or other sensitive data in the payload is a common mistake. The payload is visible to anyone who intercepts the token or has access to browser developer tools. Use JWTs for identity and authorization claims only.

8. Using Weak Signing Secrets

Using short, predictable secrets like "secret", "password", or "jwt-secret" for HMAC-based JWTs makes them vulnerable to brute-force attacks. Tools like jwt_tool and hashcat can crack weak JWT secrets in seconds. Use a cryptographically random string of at least 256 bits as your HMAC secret.

9. Not Implementing Token Refresh

If you use only long-lived access tokens, you face a dilemma: short expiry times cause frequent re-authentication, while long expiry times increase the window of vulnerability if a token is compromised. Implement a token refresh mechanism with short-lived access tokens and longer-lived refresh tokens. When the access token expires, the client uses the refresh token to obtain a new access token without requiring the user to log in again.

Programmatic JWT Decoding

While our online tool is convenient for quick inspections, you may need to decode JWTs programmatically in your applications. Here is how to decode (without verifying) a JWT in JavaScript and Python.

JavaScript (Browser or Node.js)

function decodeJWT(token) {
  const parts = token.split('.');
  if (parts.length !== 3) {
    throw new Error('Invalid JWT: expected 3 parts');
  }

  // Base64URL decode
  function base64UrlDecode(str) {
    let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
    const pad = base64.length % 4;
    if (pad) base64 += '='.repeat(4 - pad);
    return JSON.parse(atob(base64));
  }

  return {
    header: base64UrlDecode(parts[0]),
    payload: base64UrlDecode(parts[1]),
    signature: parts[2]
  };
}

// Usage
const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyJ9.signature';
const decoded = decodeJWT(token);
console.log(decoded.header);  // { alg: 'HS256' }
console.log(decoded.payload); // { sub: 'user_123' }

// Check expiry
if (decoded.payload.exp) {
  const isExpired = decoded.payload.exp * 1000 < Date.now();
  console.log('Expired:', isExpired);
}

Important: This only decodes the JWT. It does not verify the signature. Never use decoded-but-unverified claims for authorization decisions. Always verify the signature using a proper library like jsonwebtoken.

Python

import base64
import json
import time

def decode_jwt(token):
    """Decode a JWT without verifying the signature."""
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError(f'Invalid JWT: expected 3 parts, got {len(parts)}')

    def base64url_decode(s):
        # Add padding
        s += '=' * (4 - len(s) % 4)
        # Replace URL-safe characters
        s = s.replace('-', '+').replace('_', '/')
        return json.loads(base64.b64decode(s))

    header = base64url_decode(parts[0])
    payload = base64url_decode(parts[1])

    return {
        'header': header,
        'payload': payload,
        'signature': parts[2]
    }

# Usage
token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTcwMDAwMDAwMH0.sig'
decoded = decode_jwt(token)
print(decoded['header'])   # {'alg': 'HS256'}
print(decoded['payload'])  # {'sub': 'user_123', 'exp': 1700000000}

# Check expiry
if 'exp' in decoded['payload']:
    is_expired = decoded['payload']['exp'] < time.time()
    print(f'Expired: {is_expired}')

# For production use, always verify with PyJWT:
# import jwt
# decoded = jwt.decode(token, 'secret', algorithms=['HS256'])

Using Our Free JWT Decoder Tool

Now that you understand the structure, claims, algorithms, and security considerations of JSON Web Tokens, try our free JWT Decoder to inspect your own tokens. Simply paste a JWT and instantly see:

Everything runs entirely in your browser. No data is sent to any server, and no tokens are stored or logged. It is completely free with no sign-up required.

Try the JWT Decoder Now →