Definition
- JWT – JSON Web Token: a compact, URL-safe token that carries claims about a subject and is cryptographically signed.
- Header – Describes the signing algorithm (
alg: HS256 or RS256) and token type (typ: JWT). - Payload – Contains claims:
sub(subject),iss(issuer),aud(audience),exp(expiry),iat(issued at), plus custom claims like roles. - Signature – Computed over header.payload with a key; prevents tampering but does NOT hide the payload.
- Base64Url encoding – An encoding, NOT encryption; decoding is trivial and reversible by anyone.
Imagine National Bank of India issues a tamper-proof visitor pass. The top says what kind of pass it is and how the stamp was made (header). The middle lists who you are, your clearance, and when the pass expires (payload). The bottom is a holographic stamp proving the bank issued it and nobody altered it (signature). Anyone who picks up the pass can read the middle section with their eyes, so you would never print your account PIN on it; the stamp stops forgery but does not hide what is written.
Understanding the Concepts
What makes a JWT self-contained?
Everything needed to understand and trust the token travels inside it, so a service can validate without a central session store. This is what makes JWTs ideal for stateless microservices authentication.
package com.bank.jwt;
import java.util.Base64;
public class JwtStructure {
public static void main(String[] args) {
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJzdWIiOiJyYWplc2giLCJyb2xlcyI6WyJDVVNUT01FUiJdLCJpYXQiOjE3MDUzMTIyMDAsImV4cCI6MTcwNTMxMjUwMH0" +
".signature_here";
String[] parts = jwt.split("\\.");
System.out.println("JWT has 3 parts separated by dots:");
System.out.println(" Part 1 (Header): " + decode(parts[0]));
System.out.println(" Part 2 (Payload): " + decode(parts[1]));
System.out.println(" Part 3 (Signature): [cryptographic signature]");
}
private static String decode(String base64Url) {
return new String(Base64.getUrlDecoder().decode(base64Url));
}
}
Expected output:
JWT has 3 parts separated by dots:
Part 1 (Header): {"alg":"HS256","typ":"JWT"}
Part 2 (Payload): {"sub":"rajesh","roles":["CUSTOMER"],"iat":1705312200,"exp":1705312500}
Part 3 (Signature): [cryptographic signature]
What does the header contain?
The header is a small JSON object describing the algorithm and token type.
package com.bank.jwt;
public class JwtHeader {
public static void main(String[] args) {
System.out.println("Header fields:");
System.out.println(" alg: HS256 (HMAC with SHA-256) or RS256 (RSA with SHA-256)");
System.out.println(" typ: JWT");
System.out.println(" kid: key-2024-01 (optional, for key rotation)");
System.out.println();
System.out.println("HS256: shared secret, both sides can sign and verify");
System.out.println("RS256: private key signs, public key verifies");
System.out.println(" -> Better for multi-service: services only need public key");
}
}
Expected output:
Header fields:
alg: HS256 (HMAC with SHA-256) or RS256 (RSA with SHA-256)
typ: JWT
kid: key-2024-01 (optional, for key rotation)
HS256: shared secret, both sides can sign and verify
RS256: private key signs, public key verifies
-> Better for multi-service: services only need public key
What claims does the payload contain?
Claims are statements about the subject and the token. There are three kinds: registered, public, and private.
package com.bank.jwt;
public class JwtPayloadClaims {
public static void main(String[] args) {
System.out.println("Registered claims (standard short names):");
System.out.println(" iss: issuer (e.g. auth.nbi.com)");
System.out.println(" sub: subject (e.g. rajesh)");
System.out.println(" aud: audience (e.g. transfer-service)");
System.out.println(" exp: expiry time (epoch seconds)");
System.out.println(" iat: issued at time");
System.out.println(" nbf: not before time");
System.out.println(" jti: unique token ID");
System.out.println();
System.out.println("Public claims (agreed-upon names):");
System.out.println(" roles: [CUSTOMER, TELLER]");
System.out.println();
System.out.println("Private claims (app-specific):");
System.out.println(" tenant: NBI-MUMBAI");
System.out.println(" tier: PREMIUM");
}
}
Expected output:
Registered claims (standard short names):
iss: issuer (e.g. auth.nbi.com)
sub: subject (e.g. rajesh)
aud: audience (e.g. transfer-service)
exp: expiry time (epoch seconds)
iat: issued at time
nbf: not before time
jti: unique token ID
Public claims (agreed-upon names):
roles: [CUSTOMER, TELLER]
Private claims (app-specific):
tenant: NBI-MUMBAI
tier: PREMIUM
How does the signature work?
The signature is computed over the encoded header and payload with a key. A verifier recomputes it and compares; if they differ, the token was altered or forged.
package com.bank.jwt;
public class JwtSignature {
public static void main(String[] args) {
System.out.println("Signature computation:");
System.out.println(" input = base64Url(header) + '.' + base64Url(payload)");
System.out.println(" signature = HMACSHA256(input, secretKey)");
System.out.println();
System.out.println("Verification:");
System.out.println(" 1. Recompute signature from received header.payload");
System.out.println(" 2. Compare with received signature");
System.out.println(" 3. If different -> token was TAMPERED or FORGED -> reject");
System.out.println(" 4. Also check: exp (not expired), iss (correct issuer), aud (correct audience)");
}
}
Expected output:
Signature computation:
input = base64Url(header) + '.' + base64Url(payload)
signature = HMACSHA256(input, secretKey)
Verification:
1. Recompute signature from received header.payload
2. Compare with received signature
3. If different -> token was TAMPERED or FORGED -> reject
4. Also check: exp (not expired), iss (correct issuer), aud (correct audience)
Can you store sensitive information in the payload?
No. The payload is only Base64Url-encoded, not encrypted. Anyone holding the token can decode and read every claim.
package com.bank.jwt;
import java.util.Base64;
public class PayloadReadable {
public static void main(String[] args) {
// This is a real JWT payload, encoded but NOT encrypted
String encodedPayload = "eyJzdWIiOiJyYWplc2giLCJiYWxhbmNlIjo5NTAwMDB9";
String decoded = new String(Base64.getUrlDecoder().decode(encodedPayload));
System.out.println("Encoded payload: " + encodedPayload);
System.out.println("Decoded payload: " + decoded);
System.out.println();
System.out.println("ANYONE can decode this. No secret needed.");
System.out.println();
System.out.println("NEVER put in JWT payload:");
System.out.println(" - Passwords");
System.out.println(" - Full card numbers");
System.out.println(" - Government IDs (Aadhaar, PAN)");
System.out.println(" - Account balances");
System.out.println();
System.out.println("SAFE to put in JWT payload:");
System.out.println(" - Subject (username)");
System.out.println(" - Roles");
System.out.println(" - Issuer, audience, expiry");
}
}
Expected output:
Encoded payload: eyJzdWIiOiJyYWplc2giLCJiYWxhbmNlIjo5NTAwMDB9
Decoded payload: {"sub":"rajesh","balance":950000}
ANYONE can decode this. No secret needed.
What happens if someone tries to tamper with the token?
Changing any claim, like altering the role from CUSTOMER to ADMIN, breaks the signature. The verifier rejects the token.
package com.bank.jwt;
public class TamperDetection {
public static void main(String[] args) {
System.out.println("Tamper attempt:");
System.out.println(" Original payload: {sub: rajesh, roles: [CUSTOMER]}");
System.out.println(" Attacker changes: {sub: rajesh, roles: [ADMIN]}");
System.out.println(" Attacker re-encodes payload in Base64Url");
System.out.println();
System.out.println(" Verifier recomputes signature over new header.payload");
System.out.println(" Recomputed signature != original signature");
System.out.println(" -> Token REJECTED: signature mismatch");
System.out.println();
System.out.println(" The attacker cannot forge the signature without the secret key");
}
}
Expected output:
Tamper attempt:
Original payload: {sub: rajesh, roles: [CUSTOMER]}
Attacker changes: {sub: rajesh, roles: [ADMIN]}
Attacker re-encodes payload in Base64Url
Verifier recomputes signature over new header.payload
Recomputed signature != original signature
-> Token REJECTED: signature mismatch
The attacker cannot forge the signature without the secret key
How does HS256 compare to RS256?
| Aspect | HS256 | RS256 |
|---|---|---|
| Algorithm | HMAC with SHA-256 | RSA with SHA-256 |
| Key type | Shared secret | Private key signs, public key verifies |
| Who can sign | Anyone with the secret | Only the auth server (private key holder) |
| Who can verify | Anyone with the secret | Any service (public key) |
| Best for | Single service or trusted group | Multi-service distributed systems |
| Risk | If a service leaks the secret, tokens can be forged | Public key can be shared safely |
package com.bank.jwt;
public class Hs256VsRs256 {
public static void main(String[] args) {
System.out.println("HS256: shared secret");
System.out.println(" Auth server signs with secret");
System.out.println(" Every service needs the SAME secret to verify");
System.out.println(" Risk: leaked secret -> anyone can mint tokens");
System.out.println();
System.out.println("RS256: asymmetric");
System.out.println(" Auth server signs with PRIVATE key");
System.out.println(" Services verify with PUBLIC key (from JWKS endpoint)");
System.out.println(" Services CANNOT mint tokens -> safer in multi-service");
}
}
Expected output:
HS256: shared secret
Auth server signs with secret
Every service needs the SAME secret to verify
Risk: leaked secret -> anyone can mint tokens
RS256: asymmetric
Auth server signs with PRIVATE key
Services verify with PUBLIC key (from JWKS endpoint)
Services CANNOT mint tokens -> safer in multi-service
What should a senior engineer know?
Tokens cannot be easily revoked once issued. Keep them short-lived with refresh rotation. Validate aud to prevent replay. Always send over TLS.
Senior JWT guidelines:
[x] Short-lived access tokens (5 minutes)
[x] Refresh token rotation (detect reuse)
[x] Validate iss, aud, exp on every request
[x] RS256 for multi-service systems
[x] JWKS endpoint for public key distribution
[x] Always over TLS (tokens are bearer credentials)
[x] Keep payload lean (bandwidth on every request)
[x] JWE only if you truly need encrypted claims
Visual Overview
JWT Token Structure:
┌─────────────────────────────────────────────────────┐
│ JWT Token │
│ │
│ Part 1: Header (Base64Url encoded) │
│ {"alg":"RS256","typ":"JWT","kid":"key-2024-01"} │
│ │
│ Part 2: Payload (Base64Url encoded) │
│ {"sub":"rajesh", │
│ "roles":["CUSTOMER"], │
│ "iss":"auth.nbi.com", │
│ "aud":"transfer-service", │
│ "exp":1705312500, │
│ "iat":1705312200} │
│ │
│ Part 3: Signature │
│ RSA-SHA256(header.payload, privateKey) │
│ │
│ header.payload.signature (dot-separated) │
└─────────────────────────────────────────────────────┘
Signature provides: INTEGRITY (no tampering)
Signature does NOT provide: CONFIDENTIALITY (anyone can read payload)
Key Takeaway
A JSON Web Token is a compact, self-contained token of three Base64Url-encoded parts separated by dots. The header names the signing algorithm and token type. The payload holds claims: registered ones like subject, issuer, audience, and expiry, plus custom claims like roles. The signature is computed over header.payload with a key and guarantees integrity so no one can tamper with the claims.
The critical point is that the payload is only encoded, not encrypted. Anyone holding the token can decode and read every claim instantly. The signature prevents tampering but provides no confidentiality. At National Bank of India, a developer once proposed putting the account balance in the payload, and Pankaj explained that anyone holding the token could read it, so they kept only the subject and roles in the token and the sensitive data on the server.
RS256 is preferred in multi-service systems because the auth server signs with a private key and services verify with a public key from a JWKS endpoint, so services cannot mint tokens. HS256 uses a shared secret, and if any service leaks it, tokens can be forged. Tokens cannot be easily revoked once issued because their validity is self-contained, so keep them short-lived with refresh-token rotation.
Remember one sentence: a JWT is a signed, self-contained token of three Base64Url parts where the signature proves integrity but the payload is readable by anyone, so never store sensitive data in it and keep it to identity and authorization claims.
---