Toolmingo
Guides6 min read

What Is a JWT? JSON Web Tokens Explained

Learn what JSON Web Tokens (JWTs) are, how they work, how to decode and verify them in JavaScript, Python, Go, and PHP — with security pitfalls and a quick reference.

You've seen a JWT — that long, dot-separated string starting with eyJ. But what is it actually, and why does everyone use it for authentication?

This guide explains JWTs from first principles: structure, encoding, signing, verification, and the security pitfalls that trip up real-world applications.


What Is a JWT?

A JSON Web Token (JWT) is a compact, URL-safe way to represent claims between two parties. A "claim" is just a key-value pair — like userId: 42 or role: "admin".

JWTs are most commonly used for authentication and API authorization:

  1. User logs in → server creates a JWT signed with a secret
  2. Client stores the JWT (memory, localStorage, cookie)
  3. Client sends the JWT with every request (Authorization: Bearer <token>)
  4. Server verifies the signature and reads the claims — no database lookup needed
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

That's three Base64url-encoded parts, separated by dots:

<header>.<payload>.<signature>

JWT Structure

Part 1: Header

The header describes the token type and signing algorithm:

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

Common algorithms:

Algorithm Type Description
HS256 Symmetric (HMAC) Same secret for sign + verify
HS512 Symmetric (HMAC) Stronger HMAC variant
RS256 Asymmetric (RSA) Private key signs, public key verifies
ES256 Asymmetric (ECDSA) Smaller keys, fast verify
none ⚠️ DANGEROUS No signature — never accept this

Part 2: Payload

The payload holds the claims — the data you want to transmit:

{
  "sub": "1234567890",
  "name": "John Doe",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Registered claims (standardized, all optional):

Claim Full Name Description
iss Issuer Who created the token
sub Subject Who the token is about (user ID)
aud Audience Who should accept the token
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 revocation)

You can add any custom claims (role, email, plan, etc.) alongside the registered ones.

Part 3: Signature

The signature proves the token hasn't been tampered with:

HMAC-SHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

For RS256, it's signed with a private key and verified with the matching public key.


Decoding a JWT

Decoding reads the header and payload — it does not verify the signature. Use it to inspect a token. Use your JWT decoder tool for a quick paste-and-read.

JavaScript (Browser + Node.js)

// Decode without verification (inspect only — never trust unverified data)
function decodeJwt(token) {
  const [headerB64, payloadB64] = token.split('.');
  const decode = (b64) => JSON.parse(atob(b64.replace(/-/g, '+').replace(/_/g, '/')));
  return {
    header: decode(headerB64),
    payload: decode(payloadB64),
  };
}

const { header, payload } = decodeJwt('eyJhbGciOi...');
console.log(payload.sub);  // "1234567890"
console.log(new Date(payload.exp * 1000));  // expiry date

Verify with jsonwebtoken (Node.js):

import jwt from 'jsonwebtoken';

// Sign
const token = jwt.sign(
  { sub: '42', role: 'admin' },
  process.env.JWT_SECRET,
  { expiresIn: '1h' }
);

// Verify — throws if invalid or expired
try {
  const payload = jwt.verify(token, process.env.JWT_SECRET);
  console.log(payload.sub);  // "42"
} catch (err) {
  // JsonWebTokenError, TokenExpiredError, NotBeforeError
  console.error('Invalid token:', err.message);
}

Python

import jwt  # pip install PyJWT

SECRET = "your-secret-key"
ALGORITHM = "HS256"

# Sign
token = jwt.encode(
    {"sub": "42", "role": "admin"},
    SECRET,
    algorithm=ALGORITHM
)

# Verify
try:
    payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
    print(payload["sub"])  # "42"
except jwt.ExpiredSignatureError:
    print("Token expired")
except jwt.InvalidTokenError as e:
    print(f"Invalid token: {e}")

# Decode without verification (inspect only)
unverified = jwt.decode(token, options={"verify_signature": False}, algorithms=[ALGORITHM])

Go

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

type Claims struct {
    Sub  string `json:"sub"`
    Role string `json:"role"`
    jwt.RegisteredClaims
}

// Sign
claims := Claims{
    Sub:  "42",
    Role: "admin",
    RegisteredClaims: jwt.RegisteredClaims{
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
        IssuedAt:  jwt.NewNumericDate(time.Now()),
    },
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))

// Verify
parsed, err := jwt.ParseWithClaims(signed, &Claims{}, func(t *jwt.Token) (any, error) {
    if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
    }
    return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil {
    log.Fatal("invalid token:", err)
}
c := parsed.Claims.(*Claims)
fmt.Println(c.Sub)  // "42"

PHP

// composer require firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$secret = $_ENV['JWT_SECRET'];
$algorithm = 'HS256';

// Sign
$payload = [
    'sub' => '42',
    'role' => 'admin',
    'iat' => time(),
    'exp' => time() + 3600,
];
$token = JWT::encode($payload, $secret, $algorithm);

// Verify
try {
    $decoded = JWT::decode($token, new Key($secret, $algorithm));
    echo $decoded->sub;  // "42"
} catch (\Firebase\JWT\ExpiredException $e) {
    http_response_code(401);
    echo 'Token expired';
} catch (\Exception $e) {
    http_response_code(401);
    echo 'Invalid token: ' . $e->getMessage();
}

Quick Reference

Task JS (jsonwebtoken) Python (PyJWT) Go (golang-jwt) PHP (firebase/php-jwt)
Sign jwt.sign(payload, secret, opts) jwt.encode(payload, secret) jwt.NewWithClaims(...).SignedString(key) JWT::encode(payload, key, alg)
Verify jwt.verify(token, secret) jwt.decode(token, secret, algorithms=[alg]) jwt.ParseWithClaims(...) JWT::decode(token, new Key(secret, alg))
Decode only Manual Base64url split decode(..., options={"verify_signature": False}) jwt.ParseWithClaims + skip verify Manual Base64url split
Expired error TokenExpiredError ExpiredSignatureError jwt.ErrTokenExpired ExpiredException

JWT vs Session Tokens

JWT (stateless) Session (stateful)
Storage Client (cookie, memory) Server-side store (DB, Redis)
Revocation Hard — wait for expiry Easy — delete session
Database lookup per request No Yes
Scalability Great (no shared state) Needs sticky sessions or shared store
Token size Larger (hundreds of bytes) Small (opaque ID)
Best for Stateless APIs, microservices Traditional web apps, when revocation matters

6 JWT Security Mistakes

Mistake What Goes Wrong Fix
Accepting alg: none Attacker strips signature — token always valid Whitelist allowed algorithms explicitly
Storing JWT in localStorage XSS can steal the token Use HttpOnly cookies instead
No expiration (exp) Compromised token valid forever Always set a short expiry (15m–1h)
Weak secret Offline brute-force cracks HMAC Use 256-bit random secret (openssl rand -hex 32)
Trusting the payload without verifying Attacker edits claims and re-encodes Always verify signature before reading claims
Storing sensitive data in payload Payload is base64url — anyone can decode it Never put passwords, PII, or secrets in a JWT

6 FAQ

Is a JWT encrypted? No. The header and payload are Base64url-encoded, which anyone can decode. Only the signature provides integrity — it proves the data hasn't changed. If you need confidentiality, use JWE (JSON Web Encryption) or encrypt the data before putting it in the payload.

How do I invalidate a JWT before it expires? JWTs are stateless, so true revocation requires maintaining a denylist (Redis set of revoked jti values). Check the denylist on each request. Alternatively, use short expiry times plus refresh tokens.

What's the difference between iat and nbf? iat (issued at) records when the token was created. nbf (not before) sets a time before which the token is not yet valid — useful for scheduled access (e.g., a time-limited download link).

Should I use HS256 or RS256? HS256 is simpler (one shared secret) and fine for a single service that both signs and verifies. RS256 is better when multiple services need to verify tokens but only one service should be able to sign them — the verifying services only get the public key.

Why does my decoded payload show numbers for iat and exp? They're Unix timestamps (seconds since 1970-01-01 UTC). Convert with new Date(payload.exp * 1000) in JavaScript or datetime.fromtimestamp(payload['exp']) in Python.

Can I put a JWT in a query string? Technically yes, but avoid it — URLs get logged (server logs, browser history, referrer headers), which exposes the token. Prefer the Authorization: Bearer header.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools