Three segments, one dot each
A JSON Web Token is a compact, URL-safe representation of claims transferred between two parties. Its wire format is exactly three Base64URL-encoded strings joined by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 ← Header
.eyJzdWIiOiJ1c2VyLTEyMyIsInJvbGUiOiJ1c2VyIn0 ← Payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← SignatureEach segment is independently readable. The header and payload are not encrypted in a standard JWT (JWS) - they are only Base64URL-encoded, meaning anyone who intercepts the token can read its claims without any key material. Only the signature provides integrity; confidentiality requires JWE (JSON Web Encryption), an entirely separate standard.
Base64URL encoding
Standard Base64 uses +, /, and = padding - characters that require percent-encoding in URLs. Base64URL replaces them: +→-, /→_, and trailing = padding is omitted. This makes tokens safe to embed in HTTP headers, query parameters, and cookies without further escaping.
import base64, json
def b64url_decode(s):
# Restore padding
s += "=" * (-len(s) % 4)
return base64.urlsafe_b64decode(s)
token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9.SIG"
header_b64, payload_b64, sig_b64 = token.split(".")
print(json.loads(b64url_decode(header_b64)))
# → {'alg': 'RS256'}
print(json.loads(b64url_decode(payload_b64)))
# → {'sub': 'admin'}
# sig_b64 decodes to raw bytes, not JSONThe header: every field is attacker-controlled
This is the most critical point for a security engineer: the header arrives as part of the token, signed by the token bearer themselves. Before signature verification occurs, the server must parse the header to know which algorithm and key to use. This creates a bootstrapping problem - and most JWT vulnerabilities live exactly here.
{
"alg": "RS256", // DANGER: selects verification algorithm - attacker-controlled
"typ": "JWT", // token type - informational only
"kid": "key-2024", // DANGER: selects which key - path traversal / SQLi vector
"jku": "https://...",// DANGER: URL for JWKS fetch - SSRF / open-redirect bypass
"jwk": { ... }, // DANGER: embedded public key - attacker supplies their own
"x5u": "https://...",// DANGER: URL for X.509 cert chain - same risk as jku
"x5c": ["..."], // inline cert chain - similar risk to jwk
"x5t": "...", // cert thumbprint - selects cert, similar risk to kid
"crit": ["kid"] // critical extensions - must be understood or token rejected
}alg value to decide which verification function to call, rather than enforcing a pre-configured expected algorithm. An attacker who can change algto none, HS256, or any other value gains control over the entire verification path.Standard payload claims (RFC 7519)
The payload is a JSON object. RFC 7519 defines seven registered claim names, all optional. Applications typically add their own private claims (role, email, tenant_id, etc.). These private claims are the primary target of token forgery attacks.
{
"iss": "https://auth.example.com", // Issuer - who created the token
"sub": "user-123", // Subject - who the token represents
"aud": "api.example.com", // Audience - intended recipient(s)
"exp": 1735689600, // Expiration time (Unix timestamp) - MUST check
"nbf": 1735603200, // Not Before - token invalid before this time
"iat": 1735603200, // Issued At - when the token was created
"jti": "550e8400-e29b-41d4-a716-..." // JWT ID - unique identifier, replay prevention
}Servers MUST validate exp - an expired token that passes signature verification should still be rejected. Servers SHOULD validate aud to prevent a valid token for service A being replayed at service B. The jti claim enables replay prevention by tracking consumed token IDs.
Signature computation
The signature covers exactly base64url(header) + "." + base64url(payload) - the "signing input". The signature does not cover any HTTP metadata (headers, path, IP address). If an attacker can modify the header or payload and recompute a valid signature, the server has no way to distinguish the forgery from a legitimate token.
import hmac, hashlib, base64, json
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
header = b64url(json.dumps({"alg":"HS256","typ":"JWT"}).encode())
payload = b64url(json.dumps({"sub":"admin","role":"admin"}).encode())
signing_input = f"{header}.{payload}".encode()
secret = b"your-256-bit-secret"
sig = hmac.new(secret, signing_input, hashlib.sha256).digest()
token = f"{header}.{payload}.{b64url(sig)}"
print(token) # A valid HS256 JWTRS256 - Asymmetric signing
For RS256 (RSASSA-PKCS1-v1_5 with SHA-256), the server holds a private key and signs with it. Any party with the public key - often published at /.well-known/jwks.json - can verify. The critical implication: the public key is known to attackers by design, which is the foundation of the algorithm confusion attack.
ES256 - ECDSA signing
ES256 uses P-256 with SHA-256. ECDSA signatures are non-deterministic: the same message signed twice produces different signatures due to a random nonce k. A critical property for the public key recovery attack - two different ECDSA signatures cannot be used for GCD-based recovery (unlike RSA). However, a weak RNG producing repeated k values is catastrophic and has led to private key recovery in real-world cases (the PlayStation 3 ECDSA failure being the canonical example).
The JOSE family of standards
JWT is one specification in the broader JOSE (JSON Object Signing and Encryption) family:
RFC 7515 - JWS (JSON Web Signature) → signed tokens
RFC 7516 - JWE (JSON Web Encryption) → encrypted tokens
RFC 7517 - JWK (JSON Web Key) → key representation
RFC 7518 - JWA (JSON Web Algorithms) → algorithm identifiers
RFC 7519 - JWT (JSON Web Token) → claims format
RFC 7520 - JOSE cookbook → worked examples
RFC 8037 - EdDSA (Ed25519/Ed448 in JOSE) → modern curve supportA "JWT" is almost always a JWS with a JSON payload. JWE produces a token with 5 dot-separated segments and is rarely encountered in web application contexts - but when it is, it provides genuine confidentiality of the payload.
Token lifecycle and state
JWTs are stateless by design: the server need not consult a database to validate a token, because the signature is self-contained proof of authenticity. This has a major security implication: there is no built-in revocation mechanism. Once a token is issued, it is valid until its exp claim passes - unless the server maintains a denylist of jti values (negating most of the statelessness benefit) or rotates the signing key (invalidating all outstanding tokens).