? Back to Blog

Understanding JWT Tokens: Complete Security Guide

?? Published: February 24, 2026 ?? 18 min read ?? CoderTools Team

JSON Web Tokens (JWT) have become the de facto standard for authentication and authorization in modern web applications. If you're building APIs, single-page applications, or microservices, understanding JWT is essential for secure, scalable authentication.

In this comprehensive guide, you'll learn the inner workings of JWT tokens, their structure, security best practices, common vulnerabilities, debugging techniques, and implementation examples in 8+ programming languages.

?? Free Online JWT Decoder

Decode and inspect JWT tokens instantly with our free, privacy-first tool

Decode JWT Token ?

What is a JSON Web Token (JWT)?

A JSON Web Token is a compact, URL-safe means of representing claims to be transferred between two parties. JWTs are digitally signed, allowing the recipient to verify authenticity and integrity without contacting the issuer.

Key Characteristics:

Example JWT Token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT Structure: The Three Parts

Every JWT token consists of three Base64URL-encoded parts separated by dots (.):

Header.Payload.Signature

Part 1: Header

The header typically consists of two parts: the type of token (JWT) and the signing algorithm being used (HMAC SHA256, RSA, etc.).

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

Common algorithms:

Part 2: Payload (Claims)

The payload contains the claims - statements about an entity (typically, the user) and additional data. There are three types of claims:

1. Registered Claims (Standardized):

Claim Name Description
iss Issuer Who issued the token
sub Subject Who the token is about (usually user ID)
aud Audience Who should accept the token
exp Expiration When the token expires (Unix timestamp)
nbf Not Before Token not valid before this time
iat Issued At When the token was issued
jti JWT ID Unique identifier for the token

2. Public Claims:

Custom claims that are agreed upon by those using JWTs (e.g., name, email, role).

3. Private Claims:

Custom claims specific to your application.

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "john@example.com",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}
?? Critical Security Note: The payload is Base64URL-encoded, NOT encrypted. Anyone can decode and read the payload. Never store sensitive information like passwords, credit card numbers, or social security numbers in JWT payloads!

Part 3: Signature

The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way.

Signature creation process:

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

The signature ensures:

?? Decode and Inspect Your JWT Tokens

See the header, payload, and signature of any JWT token instantly

Try JWT Decoder ?

How JWT Authentication Works

The Complete Authentication Flow:

  1. User Login: User sends credentials (username/password) to authentication server
  2. Credentials Verification: Server validates credentials against database
  3. JWT Creation: Server generates JWT with user information and claims
  4. Token Return: Server sends JWT to client (usually in response body or cookie)
  5. Token Storage: Client stores JWT (localStorage, sessionStorage, or httpOnly cookie)
  6. Subsequent Requests: Client includes JWT in Authorization header (Bearer <token>)
  7. Token Verification: Server validates JWT signature and expiration
  8. Access Granted: If valid, server processes request; if invalid, returns 401 Unauthorized
// Client sends credentials
POST /api/login
{
  "username": "john@example.com",
  "password": "secretpassword"
}

// Server responds with JWT
HTTP/1.1 200 OK
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresIn": 3600
}

// Client includes JWT in subsequent requests
GET /api/user/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT Implementation in Different Languages

JavaScript / Node.js

// Using jsonwebtoken library
const jwt = require('jsonwebtoken');

// Creating a JWT
const payload = {
  sub: '1234567890',
  name: 'John Doe',
  role: 'admin'
};

const secret = 'your-256-bit-secret';
const token = jwt.sign(payload, secret, {
  expiresIn: '1h',
  algorithm: 'HS256'
});

// Verifying a JWT
try {
  const decoded = jwt.verify(token, secret);
  console.log(decoded); // { sub, name, role, iat, exp }
} catch(err) {
  console.error('Invalid token:', err.message);
}

Python

import jwt
import datetime

# Creating a JWT
payload = {
    'sub': '1234567890',
    'name': 'John Doe',
    'role': 'admin',
    'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}

secret = 'your-256-bit-secret'
token = jwt.encode(payload, secret, algorithm='HS256')

# Verifying a JWT
try:
    decoded = jwt.decode(token, secret, algorithms=['HS256'])
    print(decoded)
except jwt.ExpiredSignatureError:
    print('Token has expired')
except jwt.InvalidTokenError:
    print('Invalid token')

Java

import io.jsonwebtoken.*;
import java.util.Date;

// Creating a JWT
String secret = "your-256-bit-secret";

String token = Jwts.builder()
    .setSubject("1234567890")
    .claim("name", "John Doe")
    .claim("role", "admin")
    .setIssuedAt(new Date())
    .setExpiration(new Date(System.currentTimeMillis() + 3600000))
    .signWith(SignatureAlgorithm.HS256, secret)
    .compact();

// Verifying a JWT
try {
    Claims claims = Jwts.parser()
        .setSigningKey(secret)
        .parseClaimsJws(token)
        .getBody();
    
    System.out.println(claims.getSubject());
} catch (ExpiredJwtException e) {
    System.err.println("Token expired");
} catch (JwtException e) {
    System.err.println("Invalid token");
}

PHP

<?php
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Creating a JWT
$secret = 'your-256-bit-secret';
$payload = [
    'sub' => '1234567890',
    'name' => 'John Doe',
    'role' => 'admin',
    'exp' => time() + 3600
];

$token = JWT::encode($payload, $secret, 'HS256');

// Verifying a JWT
try {
    $decoded = JWT::decode($token, new Key($secret, 'HS256'));
    print_r($decoded);
} catch (Exception $e) {
    echo 'Invalid token: ' . $e->getMessage();
}
?>

Go

package main

import (
    "github.com/golang-jwt/jwt/v5"
    "time"
)

// Creating a JWT
func createToken() (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub": "1234567890",
        "name": "John Doe",
        "role": "admin",
        "exp": time.Now().Add(time.Hour * 1).Unix(),
    })
    
    return token.SignedString([]byte("your-256-bit-secret"))
}

// Verifying a JWT
func verifyToken(tokenString string) (*jwt.Token, error) {
    return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return []byte("your-256-bit-secret"), nil
    })
}

C# / .NET

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;

// Creating a JWT
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-256-bit-secret"));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

var claims = new[]
{
    new Claim(JwtRegisteredClaimNames.Sub, "1234567890"),
    new Claim("name", "John Doe"),
    new Claim("role", "admin")
};

var token = new JwtSecurityToken(
    expires: DateTime.Now.AddHours(1),
    claims: claims,
    signingCredentials: credentials
);

var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

// Verifying a JWT
var tokenHandler = new JwtSecurityTokenHandler();
try
{
    tokenHandler.ValidateToken(tokenString, new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = securityKey,
        ValidateIssuer = false,
        ValidateAudience = false
    }, out SecurityToken validatedToken);
}
catch (Exception ex)
{
    Console.WriteLine($"Invalid token: {ex.Message}");
}

Ruby

require 'jwt'

# Creating a JWT
payload = {
  sub: '1234567890',
  name: 'John Doe',
  role: 'admin',
  exp: Time.now.to_i + 3600
}

secret = 'your-256-bit-secret'
token = JWT.encode(payload, secret, 'HS256')

# Verifying a JWT
begin
  decoded = JWT.decode(token, secret, true, { algorithm: 'HS256' })
  puts decoded
rescue JWT::ExpiredSignature
  puts 'Token has expired'
rescue JWT::DecodeError
  puts 'Invalid token'
end

Rust

use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    name: String,
    role: String,
    exp: usize,
}

// Creating a JWT
fn create_token() -> String {
    let claims = Claims {
        sub: "1234567890".to_string(),
        name: "John Doe".to_string(),
        role: "admin".to_string(),
        exp: 10000000000,
    };
    
    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret("your-256-bit-secret".as_ref())
    ).unwrap()
}

// Verifying a JWT
fn verify_token(token: &str) -> Result {
    decode::(
        token,
        &DecodingKey::from_secret("your-256-bit-secret".as_ref()),
        &Validation::default()
    ).map(|data| data.claims)
}

JWT Security Best Practices

?? Essential Security Rules

1. Use Strong Secrets (HS256)

Your secret must be at least 256 bits (32 bytes) of random data. NEVER use simple strings like "secret" or "password".

// ? BAD - Weak secret
const secret = 'mysecret';

// ? GOOD - Strong random secret
const secret = crypto.randomBytes(32).toString('hex');
// Result: "f3d8c4b2e9a7d1f6..."

2. Always Set Expiration

Never create tokens without an expiration time. Short-lived tokens (15 minutes to 1 hour) are more secure.

// ? BAD - No expiration
jwt.sign(payload, secret);

// ? GOOD - Short expiration
jwt.sign(payload, secret, { expiresIn: '15m' });

3. Use HTTPS Only

Always transmit JWTs over HTTPS. Never send tokens over plain HTTP.

4. Store Tokens Securely

  • Best: HttpOnly cookies (prevents XSS attacks)
  • Acceptable: Memory (but lost on page refresh)
  • Avoid: localStorage (vulnerable to XSS)
  • Never: Plain cookies (vulnerable to CSRF)

5. Validate All Claims

// Verify signature, expiration, issuer, and audience
jwt.verify(token, secret, {
  algorithms: ['HS256'],
  issuer: 'your-app',
  audience: 'your-api'
});

Common JWT Vulnerabilities

1. Algorithm Confusion (alg: "none")

Attackers change the algorithm to "none" and remove the signature, hoping the server will accept unsigned tokens.

// Vulnerable code
jwt.verify(token, secret); // Uses algorithm from token header!

// Secure code
jwt.verify(token, secret, { algorithms: ['HS256'] }); // Explicit algorithm
?? Protection: Always specify allowed algorithms explicitly. Never trust the alg field in the token header.

2. Weak Secrets

Using dictionary words or short secrets makes tokens vulnerable to brute-force attacks.

// ? VULNERABLE - Common word
const secret = 'password';

// ? VULNERABLE - Short secret
const secret = 'abc123';

// ? SECURE - Strong random secret
const secret = 'f3d8c4b2e9a7d1f6c5b3a8e2d9f1c4b7e3a6d2f8c1b5e4a9d7f2c6b3e1a8d5';

3. Token Leakage

Tokens exposed in URLs, logs, or browser history can be stolen and reused.

?? Never:
  • Put tokens in URL query parameters
  • Log tokens to console or files
  • Store tokens in browser localStorage (if avoidable)
  • Send tokens to third-party analytics

4. Missing Expiration Validation

Some implementations don't properly check token expiration, allowing expired tokens to work indefinitely.

// ? VULNERABLE - Manual decode without verification
const payload = JSON.parse(atob(token.split('.')[1]));

// ? SECURE - Proper verification includes expiration check
const payload = jwt.verify(token, secret);

5. JWT in localStorage (XSS Risk)

Storing JWTs in localStorage makes them accessible to any JavaScript running on your page, including malicious scripts (XSS attacks).

?? Best Practice: Use httpOnly cookies with SameSite attribute for token storage:
res.cookie('token', jwt, {
  httpOnly: true,  // Not accessible via JavaScript
  secure: true,    // HTTPS only
  sameSite: 'strict'  // CSRF protection
});

Debugging JWT Issues

Common Problems and Solutions:

1. "Invalid Signature" Error

Causes:

Solution:

// Ensure secret matches on creation and verification
const SECRET = process.env.JWT_SECRET;

// Creation
const token = jwt.sign(payload, SECRET);

// Verification
const decoded = jwt.verify(token, SECRET);

2. "Token Expired" Error

Cause: Token's exp claim is in the past.

Solutions:

3. "Cannot Read Token" Error

Causes:

Solution:

// Extract token properly
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  throw new Error('No token provided');
}

const token = authHeader.split(' ')[1];

?? Debug Your JWT Tokens

Decode and inspect tokens to troubleshoot authentication issues

JWT Debugging Tool ?

Refresh Tokens: Extending Sessions Securely

Access tokens should be short-lived (15 minutes to 1 hour). Refresh tokens allow obtaining new access tokens without re-authentication.

Refresh Token Flow:

  1. Login: Server issues both access token (short-lived) and refresh token (long-lived)
  2. Use Access Token: Client uses access token for API requests
  3. Access Token Expires: API returns 401 Unauthorized
  4. Request New Access Token: Client sends refresh token to /refresh endpoint
  5. Validate Refresh Token: Server checks refresh token validity and user status
  6. Issue New Tokens: Server issues new access token (and optionally new refresh token)
  7. Repeat: Client continues with new access token
// Login response
{
  "accessToken": "eyJhbGci...", // Expires in 15 minutes
  "refreshToken": "eyJhbGci...", // Expires in 7 days
  "expiresIn": 900
}

// Refresh token endpoint
POST /api/refresh
{
  "refreshToken": "eyJhbGci..."
}

// Response with new access token
{
  "accessToken": "eyJhbGci...",
  "expiresIn": 900
}
?? Refresh Token Security:
  • Store refresh tokens in httpOnly cookies
  • Implement refresh token rotation (issue new refresh token on each use)
  • Store refresh tokens in database with ability to revoke
  • Add device/IP fingerprinting to detect stolen refresh tokens
  • Limit refresh token lifetime (7-30 days max)

JWT vs Sessions: When to Use Each

Feature JWT Session Cookies
Storage Client-side (stateless) Server-side (stateful)
Scalability Excellent (no server state) Requires sticky sessions or shared storage
Revocation Difficult (need blacklist) Easy (delete session)
Cross-domain Easy (add to header) Requires CORS configuration
Mobile apps Ideal Problematic (no cookie support)
Size Larger (all data in token) Smaller (just session ID)
XSS Risk High (if stored in localStorage) Low (httpOnly cookies)
CSRF Risk Low (no automatic sending) High (need CSRF tokens)

Use JWT When:

Use Session Cookies When:

Best Practices Checklist

? JWT Security Checklist:

  • ?? Use strong, randomly generated secrets (32+ bytes)
  • ?? Always set expiration time (15 min to 1 hour for access tokens)
  • ?? Explicitly specify allowed algorithms in verification
  • ?? Never store sensitive data in JWT payload
  • ?? Use HTTPS exclusively for token transmission
  • ?? Store tokens in httpOnly cookies when possible
  • ?? Implement refresh token rotation
  • ?? Add issuer (iss) and audience (aud) validation
  • ?? Use RS256 (asymmetric) for public client scenarios
  • ?? Implement token blacklist for logout functionality
  • ?? Add jti (JWT ID) for revocation tracking
  • ?? Monitor for suspicious token usage patterns

Frequently Asked Questions

Can JWT tokens be encrypted?

Yes! JWE (JSON Web Encryption) provides encryption. However, most implementations use JWS (JSON Web Signature) which only signs tokens. For sensitive payloads, use JWE or encrypt data before putting it in the JWT.

How do I invalidate/revoke a JWT?

Since JWTs are stateless, you need to maintain a token blacklist in Redis or database. Check tokens against this blacklist on each request. Alternatively, use short expiration times and refresh tokens.

Should I use HS256 or RS256?

Use HS256 (symmetric) when the token creator and verifier are the same service. Use RS256 (asymmetric) when multiple services need to verify tokens but shouldn't be able to create them.

Can I store user permissions in JWT?

Yes, but keep it minimal. Store roles/groups rather than detailed permissions. Remember: changing permissions requires issuing new tokens or waiting for expiration.

How long should JWT tokens last?

Access tokens: 15 minutes to 1 hour. Refresh tokens: 7 to 30 days. The shorter, the more secure, but also more token refreshes needed.

Is it safe to decode JWT in the browser?

Yes, decoding is safe (it's just Base64). The payload is meant to be readable. What's NOT safe is storing tokens in localStorage or exposing your secret key.

Conclusion

JWT tokens are a powerful tool for modern authentication, offering stateless, scalable, and cross-platform authentication. However, they require careful implementation to avoid security pitfalls.

Key Takeaways:

By following the security best practices in this guide, you can leverage JWT's benefits while maintaining a secure authentication system.

?? Work With JWT Tokens Now

Decode, debug, and understand JWT tokens with our free online tool

Launch JWT Decoder ?