Back to JWT: Structure, Validation, Storage
Q1Question 1 of 4

Structure of a JWT (Header, Payload, Signature) and Why the Payload Is Not Encrypted

Key Concepts
  • A JWT has three Base64Url-encoded parts separated by dots: Header, Payload, and Signature.
  • The Header specifies the signing algorithm (HS256, RS256) and token type (JWT).
  • The Payload carries claims: registered (sub, exp, iat, iss, aud, jti) and custom (role, scope).
  • The Signature is computed over Header.Payload using either a symmetric secret (HMAC) or an asymmetric private key (RSA/ECDSA).
  • The payload is only Base64Url-encoded, not encrypted: anyone who captures the token can decode and read every claim.
  • Never store secrets, passwords, card numbers, or balances in a JWT payload: use JWE if confidentiality is needed.

Definition

A JWT is signed, not sealed: the signature proves it is genuine and untampered, but everyone can read the payload.
  • JWT (JSON Web Token) – a compact, URL-safe token made of three Base64Url-encoded parts separated by dots: Header, Payload, and Signature.
  • Header – a JSON object naming the signing algorithm (alg) and token type (typ).
  • Payload – a JSON object carrying claims: registered ones like sub, exp, iat, iss, aud, jti, plus custom claims.
  • Signature – computed over base64Url(header).base64Url(payload) using either HMAC (symmetric) or RSA/ECDSA (asymmetric).
  • Not encrypted – the payload is only encoded, not encrypted; it guarantees integrity and authenticity, not confidentiality.

A JWT is like a cheque from National Bank of India. The top corner says what kind of cheque and which signing method the bank uses (header). The body shows the payee and amount: Rajesh, account SAV2001, valid until 5pm (payload). The bottom has the bank's signature (signature). Anyone who finds the cheque can read the payee and amount; the signature does not hide anything, it only proves the bank really wrote it and nobody altered it. That is why you never write a secret PIN on a cheque.

Understanding the Concepts

What is the structure of a JWT?

A JWT consists of three parts separated by dots: header.payload.signature. Each of the first two parts is a JSON object that is Base64Url-encoded. The third part is the cryptographic signature computed over the first two.

The final token looks like: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyYWplc2giLCJyb2xlIjoiQ1VTVE9NRVIifQ.HMAC_SIGNATURE

JAVA
package com.bank.security;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;

public class JwtStructureDemo {

    private static final SecretKey KEY = Keys.hmacShaKeyFor(
        "national-bank-secret-key-256-bits-long!!!".getBytes());

    public static void main(String[] args) {
        String token = Jwts.builder()
            .header().type("JWT").and()
            .subject("rajesh")
            .claim("role", "CUSTOMER")
            .claim("accountId", "SAV2001")
            .issuer("national-bank")
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + 900_000))
            .signWith(KEY)
            .compact();

        String[] parts = token.split("\\.");
        System.out.println("Header:    " + parts[0]);
        System.out.println("Payload:   " + parts[1]);
        System.out.println("Signature: " + parts[2]);
        System.out.println("Full token: " + token);
    }
}
TEXT
Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload:   eyJzdWIiOiJyYWplc2giLCJyb2xlIjoiQ1VTVE9NRVIiLCJhY2NvdW50SWQiOiJTQVYyMDAxIn0
Signature: 3kFz7P2Q5m9v8a1b2c3d4e5f6g7h8i9j
Full token: eyJhbGci...payload...signature

What is the Header?

The header is a small JSON object that tells the verifier which algorithm to use and the token type. The most common headers are {"alg":"HS256","typ":"JWT"} for symmetric signing and {"alg":"RS256","typ":"JWT"} for asymmetric signing.

The header is Base64Url-encoded to form the first segment of the token.

JAVA
import java.util.Base64;

public class HeaderDecode {
    public static void main(String[] args) {
        String headerB64 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
        String decoded = new String(Base64.getUrlDecoder().decode(headerB64));
        System.out.println("Decoded header: " + decoded);
    }
}
TEXT
Decoded header: {"alg":"HS256","typ":"JWT"}

What is the Payload and what are claims?

The payload is a JSON object containing claims. Claims are name-value pairs that carry information about the subject. There are three types:

  • Registered claims are standardized: sub (subject), exp (expiration), iat (issued at), iss (issuer), aud (audience), nbf (not before), jti (token ID).
  • Public claims are defined in registries to avoid collisions.
  • Private claims are custom: role, scope, accountId, etc.
JAVA
package com.bank.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;

public class PayloadInspector {

    public static void inspectClaims(String token, SecretKey key) {
        Claims claims = Jwts.parser()
            .verifyWith(key)
            .build()
            .parseSignedClaims(token)
            .getPayload();

        System.out.println("sub (subject):  " + claims.getSubject());
        System.out.println("iss (issuer):   " + claims.getIssuer());
        System.out.println("iat (issued):   " + claims.getIssuedAt());
        System.out.println("exp (expires):  " + claims.getExpiration());
        System.out.println("role (custom):  " + claims.get("role"));
        System.out.println("accountId:      " + claims.get("accountId"));
    }
}
TEXT
sub (subject):  rajesh
iss (issuer):   national-bank
iat (issued):   2024-10-27T10:00:00Z
exp (expires):  2024-10-27T10:15:00Z
role (custom):  CUSTOMER
accountId:      SAV2001

How does the Signature work?

The signature is computed over the Base64Url-encoded header and payload joined by a dot. For HMAC-SHA256 it is HMACSHA256(base64Url(header) + "." + base64Url(payload), secret). For RSA it uses the issuer's private key, and verification uses the public key.

If even one byte of the header or payload changes, the recomputed signature will not match, so tampering is detected.

JAVA
package com.bank.security;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import javax.crypto.SecretKey;
import java.util.Date;

public class SignatureValidationDemo {

    public static void main(String[] args) {
        SecretKey key = Keys.hmacShaKeyFor(
            "national-bank-secret-key-256-bits-long!!!".getBytes());

        String token = Jwts.builder()
            .subject("rajesh")
            .claim("role", "CUSTOMER")
            .signWith(key)
            .compact();

        // Tamper: change CUSTOMER to ADMIN in the payload
        String tampered = token.substring(0, token.lastIndexOf('.') + 1)
            + "tampered_signature";

        try {
            Jwts.parser().verifyWith(key).build().parseSignedClaims(tampered);
            System.out.println("Token accepted.");
        } catch (SignatureException e) {
            System.out.println("REJECTED: " + e.getMessage());
        }
    }
}
TEXT
REJECTED: JWT signature does not match locally computed signature.

Why is the payload NOT encrypted?

Base64Url is an encoding, not encryption. There is no key involved. Anyone can reverse it instantly with a standard Base64 decoder. The signature protects integrity (it was not changed) and authenticity (it came from the issuer), but it provides zero confidentiality.

This means anyone who captures the token in a log, a proxy, or browser history can decode and read every claim.

JAVA
package com.bank.security;

import java.util.Base64;

public class PayloadDecodeDemo {

    public static void main(String[] args) {
        // Anyone can decode the payload without any key
        String payloadB64 = "eyJzdWIiOiJyYWplc2giLCJyb2xlIjoiQ1VTVE9NRVIi"
            + "LCJhY2NvdW50SWQiOiJTQVYyMDAxIn0";
        String decoded = new String(Base64.getUrlDecoder().decode(payloadB64));
        System.out.println("Decoded payload: " + decoded);
        // Output: {"sub":"rajesh","role":"CUSTOMER","accountId":"SAV2001"}
        // No key needed. No decryption. Just encoding reversal.
    }
}
TEXT
Decoded payload: {"sub":"rajesh","role":"CUSTOMER","accountId":"SAV2001"}

What should you never store in a JWT payload?

Never store secrets, passwords, full card numbers, PINs, account balances, or any data that would be damaging if leaked. The payload should carry only a subject identifier, a role or scopes, and timing claims. Fetch anything sensitive from the database after authorization.

JAVA
// WRONG: sensitive data in JWT payload
Jwts.builder()
    .subject("rajesh")
    .claim("balance", "125000.75")       // Financial data leaks!
    .claim("cardNumber", "4532-1234-5678-9012") // PII leaks!
    .claim("pin", "1234")                // Credential leaks!
    .signWith(key).compact();

// CORRECT: only identifiers and roles
Jwts.builder()
    .subject("rajesh")
    .claim("role", "CUSTOMER")
    .issuer("national-bank")
    .signWith(key).compact();
// Fetch balance, card details from DB after verifying the token

How is HMAC different from RSA signing?

| Aspect | HMAC (HS256) | RSA (RS256) |
|---|---|---|
| Key type | Symmetric: same secret for signing and verifying | Asymmetric: private key signs, public key verifies |
| Key sharing | Every verifier knows the secret and could forge tokens | Only the auth server holds the private key |
| Performance | Faster: simple hash computation | Slower: modular exponentiation |
| Best for | Single service or trusted internal services | Microservices where verifiers should not be able to forge |
| Key rotation | Must distribute new secret to all services | Distribute new public key via JWKS endpoint |

JAVA
package com.bank.security;

import io.jsonwebtoken.Jwts;
import java.security.KeyPair;
import java.security.KeyPairGenerator;

public class RsaSigningDemo {

    public static void main(String[] args) throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048);
        KeyPair pair = gen.generateKeyPair();

        // Auth server signs with private key
        String token = Jwts.builder()
            .subject("rajesh")
            .claim("role", "CUSTOMER")
            .signWith(pair.getPrivate())
            .compact();

        // Any service verifies with public key (cannot forge)
        var claims = Jwts.parser()
            .verifyWith(pair.getPublic())
            .build()
            .parseSignedClaims(token)
            .getPayload();

        System.out.println("Verified subject: " + claims.getSubject());
    }
}
TEXT
Verified subject: rajesh

What is JWE and when do you need it?

If a payload genuinely must be confidential, you use JWE (JSON Web Encryption) instead of JWS (JSON Web Signature). JWE actually encrypts the payload so only the intended recipient can decrypt it. Plain signed JWTs (JWS) are the common case; JWE is reserved for scenarios where claims must not be visible to intermediaries.

TEXT
JWS (common):  header.payload.signature
  - Payload is Base64-encoded, readable by anyone
  - Signature proves integrity and authenticity

JWE (rare):    header.encryptedKey.iv.ciphertext.tag
  - Payload is encrypted, readable only by the intended recipient
  - Used when claims contain sensitive data that must be hidden

What should a senior engineer know?

Senior engineers pin the algorithm on the server side rather than trusting the header's alg field. Blindly trusting it opens the alg:none attack (where the attacker sets alg to none and omits the signature) and algorithm-confusion attacks (where an RSA public key is used as an HMAC secret).

Senior engineers also know that the jti claim is useful for revocation: the server maintains a small denylist of revoked token IDs and checks it for high-risk operations.

Visual Overview

DIAGRAM
  JWT Structure:
  ┌──────────────────────────────────────────────────────┐
  │                     JWT Token                         │
  │                                                       │
  │  ┌──────────────┐.┌──────────────────┐.┌──────────┐  │
  │  │   HEADER     │ │     PAYLOAD      │ │ SIGNATURE│  │
  │  │              │ │                  │ │          │  │
  │  │ alg: HS256   │ │ sub: rajesh      │ │ HMAC(    │  │
  │  │ typ: JWT     │ │ role: CUSTOMER   │ │  header  │  │
  │  │              │ │ iss: national-   │ │  +       │  │
  │  │ Base64Url    │ │      bank        │ │  payload │  │
  │  │ encoded      │ │ exp: 1730000900  │ │  , key)  │  │
  │  │              │ │                  │ │          │  │
  │  │ Base64Url    │ │ Base64Url        │ │ Base64Url│  │
  │  │ encoded      │ │ encoded          │ │ encoded  │  │
  │  └──────────────┘ └──────────────────┘ └──────────┘  │
  └──────────────────────────────────────────────────────┘
DIAGRAM
  Signed vs Encrypted:
  JWS (signed, not encrypted):
    Anyone can READ ──► Base64Url decode
    Nobody can TAMPER ──► signature check fails

  JWE (signed AND encrypted):
    Nobody can READ ──► encrypted payload
    Nobody can TAMPER ──► authentication tag fails

Key Takeaway

A JWT is a compact token with three Base64Url-encoded parts separated by dots. The header names the algorithm and token type, the payload carries claims such as sub for the subject, exp for expiry, iat for issued-at, iss for issuer, plus any custom claims like role, and the signature is computed over the header and payload using either an HMAC secret or an RSA private key. The signature is what makes the token trustworthy: if anyone changes a single byte of the header or payload, the recomputed signature will not match and the token is rejected.

In the National Bank of India project, each microservice verifies the signature locally with the shared key, so it can trust a token without calling back to the login server. A tampered token where the role was changed from CUSTOMER to ADMIN fails the signature check immediately, which is exactly the integrity protection the system relies on.

The critical point is why the payload is not encrypted. Base64Url is only an encoding, not encryption; there is no key, so anyone who captures the token can decode and read every claim instantly. The signature protects integrity and authenticity, meaning the token was not altered and really came from the issuer, but it provides zero confidentiality.

That is why in a bank the JWT payload never contains secrets, passwords, full card numbers, or balances. It carries only an identifier, a role, and expiry, and sensitive data is fetched from the database behind proper authorization. If a payload genuinely must be confidential, JWE (JSON Web Encryption) is the right tool, but for normal stateless auth a signed JWT with a short expiry and no secrets in the claims is the correct design.

Remember one sentence: a JWT is signed for integrity, not encrypted for secrecy, so anyone can read the payload but nobody can change it.

---

Share
Use ← → arrow keys to navigate