Authentication Guide

Understanding JWT Tokens:
Complete Developer Guide

Everything you need to know about JSON Web Tokens. Learn the structure, authentication flows, security best practices, and implementation patterns.

18 min read Updated Jan 2026

1. What is a JWT Token?

JWT (JSON Web Token) is an open standard (RFC 7519) for securely transmitting information between parties as a JSON object. JWTs are digitally signed, so the information can be verified and trusted.

Example JWT Token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
■ Header ■ Payload ■ Signature

Key Characteristics of JWT:

  • Compact: Small size allows transmission in URLs, headers, or POST parameters
  • Self-contained: Contains all the information needed about the user
  • Stateless: Server doesn't need to store session data
  • Verifiable: Digital signature ensures data integrity

Common Use Cases:

  • Authentication: After login, each request includes the JWT for access control
  • Information Exchange: Securely transmit data between parties
  • Authorization: Determine what resources a user can access
  • Single Sign-On (SSO): Share authentication across multiple domains

2. JWT Structure (Header, Payload, Signature)

A JWT consists of three parts separated by dots (.): xxxxx.yyyyy.zzzzz

1. Header

Contains metadata about the token: algorithm and token type.

{
  "alg": "HS256",    // Signing algorithm
  "typ": "JWT"       // Token type
}

Base64URL encoded → eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Payload (Claims)

Contains the claims - statements about the user and additional metadata.

{
  "sub": "1234567890",      // Subject (user ID)
  "name": "John Doe",       // Custom claim
  "admin": true,            // Custom claim
  "iat": 1516239022,        // Issued at
  "exp": 1516242622         // Expiration time
}

Base64URL encoded → eyJzdWIiOiIxMjM0NTY3ODkwIi4uLn0

3. Signature

Verifies the token wasn't tampered with. Created by signing header + payload.

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

Result → SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

⚠️ Important

The header and payload are only Base64URL encoded, NOT encrypted. Anyone can decode and read them. Never put sensitive data (passwords, credit cards) in a JWT payload!

3. JWT Authentication Flow

Step-by-step Authentication Flow:

  1. Login Request: User sends credentials (email/password) to server
  2. Verification: Server validates credentials against database
  3. Token Creation: Server creates JWT with user claims and signs it
  4. Token Response: Server sends JWT back to client
  5. Token Storage: Client stores JWT (cookie or localStorage)
  6. Authenticated Request: Client sends JWT with each request (Authorization header)
  7. Token Validation: Server verifies JWT signature and checks expiration
  8. Resource Access: Server grants/denies access based on claims

Request Header Format:

// HTTP Authorization Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIi4uLn0.signature

Refresh Token Flow:

For better security, use short-lived access tokens with longer-lived refresh tokens:

  • Access Token: Short-lived (5-15 minutes), used for API requests
  • Refresh Token: Long-lived (days/weeks), used to get new access tokens

4. Standard JWT Claims Explained

Claim Name Description
iss Issuer Who created the token (e.g., your auth server)
sub Subject Who the token represents (usually user ID)
aud Audience Intended recipient (e.g., API server)
exp Expiration Unix timestamp when token expires
nbf Not Before Token not valid before this time
iat Issued At When the token was created
jti JWT ID Unique identifier for the token

Custom Claims:

You can add any custom claims for your application:

{
  "sub": "user_123",
  "email": "john@example.com",
  "role": "admin",
  "permissions": ["read", "write", "delete"],
  "organization_id": "org_456"
}

5. Signing Algorithms (HS256, RS256, ES256)

Symmetric (HMAC)

Same secret key for signing and verification

  • HS256: HMAC + SHA-256
  • HS384: HMAC + SHA-384
  • HS512: HMAC + SHA-512

Best for: Single server, internal APIs

Asymmetric (RSA/ECDSA)

Private key signs, public key verifies

  • RS256: RSA + SHA-256
  • ES256: ECDSA + P-256
  • PS256: RSA-PSS + SHA-256

Best for: Microservices, distributed systems

Algorithm Comparison:

Algorithm Type Key Size Recommended
HS256 Symmetric 256-bit secret ✅ Single server
RS256 Asymmetric 2048-bit RSA ✅ Distributed
ES256 Asymmetric 256-bit EC ✅ Modern choice
none - - ❌ Never use!

6. Implementation in Different Languages

JavaScript (Node.js with jsonwebtoken)

const jwt = require('jsonwebtoken');

// Secret key (use environment variable in production!)
const SECRET = process.env.JWT_SECRET;

// Create a token
function createToken(user) {
    const payload = {
        sub: user.id,
        email: user.email,
        role: user.role
    };
    
    return jwt.sign(payload, SECRET, {
        expiresIn: '15m',    // Short-lived access token
        issuer: 'myapp.com',
        audience: 'myapp.com'
    });
}

// Verify a token
function verifyToken(token) {
    try {
        const decoded = jwt.verify(token, SECRET, {
            issuer: 'myapp.com',
            audience: 'myapp.com'
        });
        return { valid: true, payload: decoded };
    } catch (err) {
        return { valid: false, error: err.message };
    }
}

// Express middleware
function authMiddleware(req, res, next) {
    const authHeader = req.headers.authorization;
    if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'No token provided' });
    }
    
    const token = authHeader.split(' ')[1];
    const result = verifyToken(token);
    
    if (!result.valid) {
        return res.status(401).json({ error: result.error });
    }
    
    req.user = result.payload;
    next();
}

Python (PyJWT)

import jwt
from datetime import datetime, timedelta
import os

SECRET = os.environ.get('JWT_SECRET')

def create_token(user: dict) -> str:
    """Create a JWT token for a user."""
    payload = {
        'sub': user['id'],
        'email': user['email'],
        'role': user['role'],
        'iat': datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(minutes=15)
    }
    return jwt.encode(payload, SECRET, algorithm='HS256')

def verify_token(token: str) -> dict:
    """Verify and decode a JWT token."""
    try:
        payload = jwt.decode(token, SECRET, algorithms=['HS256'])
        return {'valid': True, 'payload': payload}
    except jwt.ExpiredSignatureError:
        return {'valid': False, 'error': 'Token expired'}
    except jwt.InvalidTokenError as e:
        return {'valid': False, 'error': str(e)}

# Flask decorator
from functools import wraps
from flask import request, jsonify

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get('Authorization', '')
        if not auth_header.startswith('Bearer '):
            return jsonify({'error': 'No token provided'}), 401
        
        token = auth_header.split(' ')[1]
        result = verify_token(token)
        
        if not result['valid']:
            return jsonify({'error': result['error']}), 401
        
        request.user = result['payload']
        return f(*args, **kwargs)
    return decorated

Java (jjwt)

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtUtil {
    private static final Key SECRET = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final long EXPIRATION_MS = 15 * 60 * 1000; // 15 minutes
    
    public static String createToken(User user) {
        return Jwts.builder()
            .setSubject(user.getId())
            .claim("email", user.getEmail())
            .claim("role", user.getRole())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
            .signWith(SECRET)
            .compact();
    }
    
    public static Claims verifyToken(String token) {
        try {
            return Jwts.parserBuilder()
                .setSigningKey(SECRET)
                .build()
                .parseClaimsJws(token)
                .getBody();
        } catch (JwtException e) {
            throw new RuntimeException("Invalid token: " + e.getMessage());
        }
    }
}

7. Security Best Practices

✅ Do: Use Strong Secrets

For HS256, use at least 256-bit (32 character) random secrets. Generate with: openssl rand -hex 32

✅ Do: Set Short Expiration

Access tokens: 5-15 minutes. Use refresh tokens for longer sessions. Shorter = less damage if compromised.

✅ Do: Validate All Claims

Always verify: signature, expiration (exp), issuer (iss), audience (aud). Don't just decode - validate!

✅ Do: Use HTTPS

Always transmit JWTs over HTTPS. HTTP allows token interception via man-in-the-middle attacks.

❌ Don't: Store Sensitive Data

Never put passwords, credit cards, or PII in JWTs. The payload is readable by anyone!

❌ Don't: Use Weak Secrets

Never use "secret", "password", or short strings. These can be brute-forced in seconds.

❌ Don't: Accept Unsigned Tokens

Explicitly reject tokens with "alg": "none". Always specify allowed algorithms.

8. Common JWT Vulnerabilities

CVE Algorithm Confusion Attack

Attacker changes alg from RS256 to HS256 and signs with the public key (treating it as HMAC secret).

Fix: Always specify allowed algorithms: algorithms=['RS256']

CVE None Algorithm Attack

Attacker sets alg to "none" and removes signature. Vulnerable libraries accept unsigned tokens.

Fix: Reject tokens with "alg": "none". Use updated libraries.

RISK Weak Secret Key

Short or predictable secrets can be brute-forced. "secret" or "password" cracked in seconds.

Fix: Use 256+ bit random secrets: openssl rand -hex 32

RISK Token Sidejacking

Stolen tokens via XSS or network sniffing can be replayed by attackers.

Fix: Use HttpOnly cookies, short expiration, token binding, and HTTPS.

9. Token Storage: Cookies vs localStorage

Storage Method XSS Protection CSRF Protection Best For
HttpOnly Cookie ✅ Protected ❌ Vulnerable Most secure (with CSRF tokens)
localStorage ❌ Vulnerable ✅ Protected SPAs with strict CSP
sessionStorage ❌ Vulnerable ✅ Protected Tab-specific sessions
Memory (variable) ✅ Protected ✅ Protected Most secure (lost on refresh)

Recommended: HttpOnly Cookie with SameSite

// Setting a secure HttpOnly cookie (Node.js/Express)
res.cookie('accessToken', token, {
    httpOnly: true,        // Not accessible via JavaScript
    secure: true,          // HTTPS only
    sameSite: 'strict',    // CSRF protection
    maxAge: 15 * 60 * 1000 // 15 minutes
});

10. Frequently Asked Questions

What is a JWT token used for?
JWTs are primarily used for authentication and authorization. After login, the server issues a JWT that the client includes with each request. The server validates the token to grant access to protected resources. JWTs are also used for single sign-on (SSO) and secure information exchange between parties.
Is JWT more secure than sessions?
Neither is inherently more secure - both have trade-offs. Sessions are stateful (stored server-side), making revocation easy but requiring database lookups. JWTs are stateless (no server storage) but harder to revoke. JWTs are better for distributed systems; sessions are better when you need instant revocation. Security depends on proper implementation.
How do I invalidate/revoke a JWT?
Since JWTs are stateless, revocation requires additional infrastructure: (1) Token blocklist: Store revoked token IDs in Redis/database and check on each request. (2) Short expiration: Use 5-15 minute tokens so compromised tokens expire quickly. (3) Refresh token rotation: Invalidate refresh tokens on logout. (4) Version claim: Add a version to users and tokens; increment on logout.
What's the difference between HS256 and RS256?
HS256 (symmetric): Uses the same secret for signing and verification. The secret must be shared with all verifiers. Best for single-server setups.

RS256 (asymmetric): Uses a private key to sign and a public key to verify. Only the auth server needs the private key; others can verify with the public key. Best for microservices and distributed systems.
Should I store JWT in localStorage or cookies?
HttpOnly cookies are more secure. They're not accessible via JavaScript, protecting against XSS attacks. localStorage is vulnerable to XSS but immune to CSRF. For best security: use HttpOnly cookies with SameSite=Strict (or Lax) and implement CSRF tokens. If using localStorage, ensure strict Content Security Policy.
How long should a JWT be valid?
Access tokens: 5-15 minutes. Short-lived to minimize damage if stolen.
Refresh tokens: 7-30 days (stored securely, rotated on use).
Critical applications (banking) may use even shorter access tokens (1-5 minutes). The key is balancing security (shorter) vs user experience (longer).
Can JWT be decoded without the secret?
Yes! The header and payload are just Base64URL encoded, not encrypted. Anyone can decode and read them. The signature only ensures the data wasn't tampered with - it doesn't hide the content. Never put sensitive information (passwords, credit card numbers) in JWT payloads. Use our JWT Decoder to see any token's contents.
What is a refresh token and why use it?
A refresh token is a long-lived token used to obtain new access tokens without re-authentication. Workflow: (1) User logs in → gets access token (15 min) + refresh token (7 days). (2) Access token expires → client uses refresh token to get new access token. (3) User stays logged in without entering password again. This combines security (short-lived access) with convenience (long sessions).
JWT vs OAuth vs OpenID Connect - what's the difference?
JWT: A token format - how to structure and sign tokens.
OAuth 2.0: An authorization framework - how to grant third-party access to resources without sharing passwords.
OpenID Connect: An identity layer on top of OAuth 2.0 - adds authentication and user identity.
OAuth and OIDC often use JWTs as their token format, but they're separate concepts.
What happens when a JWT expires?
When the exp claim timestamp is in the past, the token is invalid. The server should reject it with a 401 Unauthorized error. The client should: (1) Attempt to use the refresh token to get a new access token, or (2) Redirect the user to login again if no valid refresh token exists. Implement token refresh logic before the access token expires for seamless UX.

Need to decode a JWT?

Try our free JWT decoder tool. Decode tokens instantly, validate signatures, and inspect claims - all client-side.

Open JWT Decoder

Related Guides