Understanding JWT Tokens: Complete Security Guide
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:
- Self-contained: Contains all necessary information about the user
- Stateless: Server doesn't need to store session data
- Digitally signed: Can verify authenticity and detect tampering
- Compact: Can be transmitted via URL, POST parameter, or HTTP header
- Cross-domain: Works across different domains (CORS-friendly)
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:
- HS256 (HMAC SHA-256): Symmetric signing with a shared secret
- RS256 (RSA SHA-256): Asymmetric signing with public/private key pair
- ES256 (ECDSA SHA-256): Elliptic curve digital signature
- none: Unsecured JWT (?? NEVER use in production!)
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
}
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:
- Authenticity: Token was issued by the legitimate server
- Integrity: Token hasn't been tampered with
- Non-repudiation: Issuer cannot deny creating the token
?? 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:
- User Login: User sends credentials (username/password) to authentication server
- Credentials Verification: Server validates credentials against database
- JWT Creation: Server generates JWT with user information and claims
- Token Return: Server sends JWT to client (usually in response body or cookie)
- Token Storage: Client stores JWT (localStorage, sessionStorage, or httpOnly cookie)
- Subsequent Requests: Client includes JWT in Authorization header (
Bearer <token>) - Token Verification: Server validates JWT signature and expiration
- 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
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.
- 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).
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:
- Wrong secret used for verification
- Token was tampered with
- Token was created with different algorithm than verification expects
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:
- Implement refresh token mechanism
- Check system clock synchronization (servers must have accurate time)
- Add clock tolerance in verification (
clockTolerance: 30seconds)
3. "Cannot Read Token" Error
Causes:
- Token missing from request
- Wrong header format (should be
Bearer <token>) - Token corrupted in transit
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:
- Login: Server issues both access token (short-lived) and refresh token (long-lived)
- Use Access Token: Client uses access token for API requests
- Access Token Expires: API returns 401 Unauthorized
- Request New Access Token: Client sends refresh token to
/refreshendpoint - Validate Refresh Token: Server checks refresh token validity and user status
- Issue New Tokens: Server issues new access token (and optionally new refresh token)
- 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
}
- 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:
- Building microservices architecture
- Need horizontal scalability without shared state
- Developing mobile apps or SPAs
- Implementing single sign-on (SSO) across domains
- Building APIs consumed by third parties
Use Session Cookies When:
- Need instant token revocation
- Traditional web application (server-side rendering)
- Highly sensitive applications (banking)
- Need to store large amounts of session data
- Simple authentication requirements
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:
- JWTs are self-contained, signed tokens with header, payload, and signature
- Never store sensitive data in JWT payloads (they're not encrypted by default)
- Always use strong secrets, explicit algorithms, and short expiration times
- Prefer httpOnly cookies over localStorage for token storage
- Implement refresh tokens for secure session extension
- Understand the tradeoffs between JWT and traditional sessions
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 ?