JWT
Also known as: JSON Web Token, JWTs, bearer token
A compact, self-contained token format that encodes claims as a signed JSON object — widely used for stateless authentication and authorisation between services.
- Primary domain
- Security & Privacy
- Sub-category
- Cryptography & Formal Methods
In simple terms
A JWT is a three-part token (header.payload.signature) that proves identity without a server-side session lookup. The server signs the payload with its secret key; anyone can verify the signature but only the key holder can create valid tokens. When a microservice receives a JWT, it validates the signature locally — no round-trip to a session store needed.
The Visual Map
flowchart TD
subgraph token["JWT: header.payload.signature"]
H["Header\n{alg: HS256, typ: JWT}\nbase64url encoded"]
P["Payload\n{sub, roles, exp, iat}\nbase64url encoded"]
S["Signature\nHMAC-SHA256(header.payload, secret)\nor RS256(RSA private key)"]
end
ISSUE["Auth server\n(signs token on login)"]
SVC["API service\n(verifies token on every request)"]
ISSUE -->|"1. sign and issue"| token
token -->|"2. Bearer token in Authorization header"| SVC
SVC -->|"3. verify signature + check exp"| DECIDE["Allow or deny request"]
More detail
Structure — three base64url-encoded JSON blobs joined by dots:
- Header: algorithm (
alg) and token type (typ: JWT). Common:HS256(symmetric HMAC-SHA256),RS256(RSA asymmetric),ES256(ECDSA). - Payload (claims): registered claims —
sub(subject),exp(expiry),iat(issued-at),iss(issuer),aud(audience). Plus arbitrary application claims (user ID, roles). - Signature: over
base64url(header) + "." + base64url(payload). Changing a payload byte invalidates the signature.
Verification steps (never skip any):
- Decode and check the header
alg— rejectalg: noneoutright. - Verify the signature using the correct key for the stated algorithm.
- Check
exphas not passed. - Check
issmatches the expected issuer. - Check
audmatches your service (prevents token substitution attacks).
HS256 vs RS256:
- HS256 — HMAC with a shared secret. Simple, fast, symmetric. All verifying services need the secret, so token issuance and verification share trust. Good for single-service use.
- RS256 / ES256 — asymmetric. Auth server signs with private key; services verify with public key. Scales to many services without sharing secrets. The private key never leaves the auth server.
Common vulnerabilities:
alg: noneattack — some early libraries accepted tokens with no signature ifalgwasnone. Always validatealgbefore verification.- Key confusion attack — RS256 public key used as HS256 secret. Reject if
algdoesn’t match expected. - Missing
expcheck — tokens last forever, no revocation needed. - Storing JWTs in
localStorage— accessible to XSS. PreferHttpOnlycookies for browser clients.
Under the Hood
JWT generation and verification using only Python stdlib — HS256 (HMAC-SHA256):
import base64, hmac, hashlib, json, time
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
def decode_b64url(s: str) -> bytes:
return base64.urlsafe_b64decode(s + '==')
def make_jwt(payload: dict, secret: bytes) -> str:
header = b64url(json.dumps({"alg":"HS256","typ":"JWT"}).encode())
body = b64url(json.dumps(payload).encode())
sig_msg = f"{header}.{body}".encode()
sig = b64url(hmac.new(secret, sig_msg, hashlib.sha256).digest())
return f"{header}.{body}.{sig}"
def verify_jwt(token: str, secret: bytes) -> dict | None:
parts = token.split('.')
if len(parts) != 3: return None
h, b, s = parts
expected = b64url(hmac.new(secret, f"{h}.{b}".encode(), hashlib.sha256).digest())
if not hmac.compare_digest(expected, s): return None
payload = json.loads(decode_b64url(b))
if payload.get('exp', float('inf')) < time.time(): return None
return payload
secret = b"super-secret-key-32-bytes-minimum"
token = make_jwt({"sub": "user123", "roles": ["admin"],
"exp": int(time.time()) + 3600}, secret)
print("Token:", token[:50], "...")
print()
h, b, _ = token.split('.')
print("Header: ", json.loads(decode_b64url(h)))
print("Payload:", json.loads(decode_b64url(b)))
print()
print("Valid: ", verify_jwt(token, secret) is not None)
print("Wrong key:", verify_jwt(token, b"wrong-key") is None)
tampered_payload = b64url(json.dumps({"sub":"user123","roles":["superadmin"],"exp":int(time.time())+3600}).encode())
tampered = token.split('.')[0] + '.' + tampered_payload + '.' + token.split('.')[2]
print("Tampered: ", verify_jwt(tampered, secret) is None)
Engineering Trade-offs
- Stateless JWTs vs server sessions. JWTs require no session store, ideal for microservices. The trade-off: you cannot revoke a token before it expires without a token blocklist — reintroducing server-side state. Short expiry times (15 min access token + refresh token) mitigate this.
- HS256 vs RS256. HS256 is simpler and faster; RS256 allows public verification without secret sharing. In microservice architectures with many verifying services, RS256 is strongly preferred.
- JWT payload size. JWTs are sent with every request. Bloating the payload with roles/permissions increases bandwidth. Keep payloads minimal; fetch permissions on the server if they’re large.
- Cookie vs Authorization header.
Authorization: Bearer <jwt>is accessible to JavaScript and vulnerable to XSS theft.HttpOnly; SameSite=Strict; Securecookies are invisible to JS and CSRF-resistant, but require SPA-friendly handling.
Real-world examples
- Auth0, Okta, and AWS Cognito issue JWTs (specifically OIDC ID tokens and access tokens) after authentication.
- Kubernetes RBAC uses JWTs (service account tokens) to authenticate API calls from pods.
- GitHub Actions uses ephemeral RS256 JWTs (OIDC tokens) so workflows can authenticate to AWS/GCP without stored secrets.
- Firebase uses HS256 JWTs for mobile app authentication.
Common misconceptions
- “JWT payloads are encrypted.” They are only base64url-encoded — anyone who receives the token can read the payload. Use JWE (JSON Web Encryption) if the payload is sensitive.
- “JWT = secure session.” A JWT proves the token was issued by the signer. It doesn’t prove the user is still active or hasn’t been compromised since issuance. Plan for revocation.
Try it yourself
Sign, inspect, and verify a JWT — then tamper with the payload to see the signature fail:
python3 -c "
import base64, hmac, hashlib, json, time
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b'=').decode()
dec = lambda s: base64.urlsafe_b64decode(s+'==')
def sign(payload, key):
h = b64(json.dumps({'alg':'HS256','typ':'JWT'}).encode())
p = b64(json.dumps(payload).encode())
s = b64(hmac.new(key, f'{h}.{p}'.encode(), hashlib.sha256).digest())
return f'{h}.{p}.{s}'
def verify(tok, key):
h,p,s = tok.split('.')
ok = hmac.compare_digest(b64(hmac.new(key,f'{h}.{p}'.encode(),hashlib.sha256).digest()),s)
return json.loads(dec(p)) if ok else None
key = b'secret'
tok = sign({'sub':'alice','roles':['admin'],'exp':int(time.time())+3600}, key)
print('Valid: ', verify(tok,key) is not None)
print('Bad key: ', verify(tok,b'other') is None)
parts=tok.split('.')
tampered=parts[0]+'.'+b64(json.dumps({'sub':'alice','roles':['superadmin'],'exp':int(time.time())+3600}).encode())+'.'+parts[2]
print('Tampered:', verify(tampered,key) is None)
payload=json.loads(dec(parts[1]))
print('Payload: ', payload)
"
Learn next
- OAuth — JWTs are the access token format used in OAuth 2.0 and OIDC flows.
- Authorization — JWTs carry the claims (roles, scopes) that drive authorisation decisions.
- Authentication — the login flow that issues a JWT in the first place.
- TLS — JWTs are sent over TLS; without it, they can be stolen in transit.
Relationships
- Requires
- Related
- Next
- Leads to
Neighborhood
A visual companion to the relationships above. Click any node to visit that topic.