DevLab
Encoding

How JWT Works: Header, Payload, Signature Decoded

Understand JWT structure, how HS256 and RS256 signing work, when to use JWT over sessions, and the security mistakes that get apps breached.

What a JWT Looks Like

A JSON Web Token is three Base64URL-encoded strings joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDAyMDAwMDAsImV4cCI6MTcwMDIwMzYwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Paste that into the DevLab JWT Decoder and you see three JSON objects. The third part — the signature — cannot be decoded into readable JSON; it is a cryptographic value. But the first two are just Base64URL encoding of plain JSON.

The Header

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

alg specifies the signing algorithm. typ is always "JWT". These two fields tell the verifying party how to check the signature.

The Payload (Claims)

{
  "sub": "user_123",
  "email": "alice@example.com",
  "iat": 1700200000,
  "exp": 1700203600,
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com"
}

Standard registered claims (defined in RFC 7519):

  • sub — Subject: who the token is about (usually a user ID)
  • iat — Issued At: Unix timestamp when the token was created
  • exp — Expiration: Unix timestamp after which the token is invalid
  • iss — Issuer: who created the token
  • aud — Audience: who the token is intended for
  • nbf — Not Before: token is invalid before this time
  • jti — JWT ID: unique identifier to prevent replay attacks

You can add any custom claim: "role": "admin", "org_id": 42. The payload is NOT encrypted — anyone with the token can read these values. Never put passwords, credit card numbers, or PII you cannot expose.

The Signature

The signature proves the token was issued by someone who knows the secret, and that the header and payload have not been modified since signing:

signature = HMACSHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

If anyone changes a single character in the header or payload, the signature will not match. The verifier rejects the token.

HS256 vs RS256: Symmetric vs Asymmetric Signing

This is the most important architectural decision when implementing JWT:

HS256 (HMAC-SHA256 — symmetric):

  • Uses one shared secret. Both the issuer and every verifier need the same secret.
  • Fast and simple for single-service architectures.
  • Problem: if you have 10 microservices all verifying JWTs, all 10 must hold the secret. A breach of any service exposes the signing key — an attacker can forge arbitrary tokens.

RS256 (RSA-SHA256 — asymmetric):

  • Uses a public/private key pair. The auth server signs with its private key. All other services verify with the public key.
  • The public key can be published openly (e.g., at /.well-known/jwks.json) — compromising a verifier does not expose the signing secret.
  • The right choice for microservices, third-party integrations, and any system where verification happens outside the signing authority.
// Verify with RS256 in Node.js (using jose library)
import { jwtVerify, createRemoteJWKSet } from 'jose'
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'))
const { payload } = await jwtVerify(token, JWKS, {
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
})

JWT vs Sessions: When to Use Which

Use sessions when:

  • You need instant revocation — a stolen session can be deleted server-side immediately
  • You have a single backend server or a shared session store (Redis)
  • You are building a traditional server-rendered web app
  • Token size matters — a session ID is 32 bytes vs a JWT at 200-500+ bytes sent on every request

Use JWTs when:

  • Multiple independent services need to verify identity without calling a central auth server
  • You are building a stateless API consumed by mobile apps or SPAs
  • You need cross-domain authentication
  • Short-lived tokens (15-minute expiry) with refresh token rotation are acceptable

Security Mistakes That Get Apps Breached

  • Algorithm confusion ("alg: none" attack): Early JWT libraries trusted the alg header field from the token itself. Setting alg: "none" bypassed signature verification entirely. Always specify the expected algorithm explicitly when verifying — never trust the header.
  • Storing JWTs in localStorage: XSS attacks can steal localStorage values. Use httpOnly cookies so JavaScript cannot access the token at all.
  • Long-lived tokens without rotation: A JWT stolen today is valid for however long the expiry is set. Use 15-minute access tokens with refresh token rotation to limit the window of exposure.
  • Not verifying the exp claim: Always check expiration server-side. Some libraries verify signature but skip expiry validation if you do not configure it.
  • Not validating iss and aud: A valid JWT from your login service should not be accepted by your payment service. Always verify issuer and audience.
  • Sensitive data in payload: The payload is Base64-encoded, not encrypted. Any party holding the token can read it without the secret key.

Verifying a JWT in Practice

// Node.js with jsonwebtoken
import jwt from 'jsonwebtoken'

// Sign
const token = jwt.sign(
  { sub: 'user_123', email: 'alice@example.com' },
  process.env.JWT_SECRET,
  { expiresIn: '15m', issuer: 'https://auth.example.com' }
)

// Verify (ALWAYS specify algorithms explicitly)
try {
  const payload = jwt.verify(token, process.env.JWT_SECRET, {
    algorithms: ['HS256'],
    issuer: 'https://auth.example.com',
  })
  // payload.sub === 'user_123'
} catch (err) {
  // TokenExpiredError, JsonWebTokenError, NotBeforeError
  console.error('Invalid token:', err.message)
}

Frequently Asked Questions

Is it safe to store sensitive data in a JWT?

No. A standard JWT (using JWS) is signed but not encrypted — the payload is Base64url-encoded, which is trivially decoded by anyone who has the token. Never put passwords, credit card numbers, or personally identifiable information in a JWT payload. If you need encrypted tokens, use JWE (JSON Web Encryption), but consider whether a server-side session with a random ID would be simpler and safer. The JWT payload should contain only claims needed for authorization: user ID, roles, expiration time.

What happens if a JWT is stolen?

A stolen JWT can be used by anyone who has it until it expires — the server has no way to distinguish the legitimate user from the attacker because JWTs are self-contained and stateless. Mitigations include: short expiration times (15 minutes or less for access tokens), refresh token rotation (issue a new refresh token with each use and invalidate the old one), binding tokens to a client fingerprint or IP range, and maintaining a server-side deny list for compromised tokens. The last option partially defeats the stateless benefit of JWTs, which is why many security engineers recommend server-side sessions for applications where revocation matters.

What is the difference between HS256 and RS256 signing?

HS256 (HMAC-SHA256) uses a single shared secret for both signing and verification. Both the token issuer and verifier must know the same secret, which means the secret must be distributed securely to every service that needs to validate tokens. RS256 (RSA-SHA256) uses an asymmetric key pair: the issuer signs with a private key, and verifiers use the corresponding public key. The public key can be distributed freely (typically via a JWKS endpoint), so verifying services never need access to the signing key. RS256 is standard for multi-service architectures and OAuth/OIDC; HS256 is simpler for single-service setups where key distribution is not a concern.

Practice with these tools

More Learning Topics

RegexRegex Basics: A Complete Beginner's GuideRegexRegex Special Characters: Complete ReferenceRegexRegex Groups and Captures ExplainedRegexRegex Quantifiers: Complete GuideCSSCSS Selectors: The Complete GuideCSSCSS Specificity: Why Your Styles Aren't ApplyingJSONJSONPath Syntax: Query JSON Like XPathTimeUnix Timestamps ExplainedEncodingBase64 Encoding ExplainedEncodingJWT Structure and How It WorksEncodingJWT vs Session Tokens: Which Should You Use?EncodingJWT Refresh Tokens ExplainedCryptoHash Functions Explained: MD5, SHA-256, and When to Use EachEncodingURL Encoding Explained: What %20 Actually MeansJSONJSON Schema Explained: Validate Your JSON DataJSONJSON vs YAML: Which Should You Use?JSONJSON.stringify and JSON.parse: Edge Cases You Should KnowRegexRegex Lookahead and Lookbehind: Match Without ConsumingRegexRegex for Email Validation: The Right ApproachCSSThe CSS Box Model: margin, padding, border, and contentCSSFlexbox vs CSS Grid: When to Use EachCSSCSS Custom Properties (Variables) ExplainedTimeISO 8601 Explained: The Right Way to Format DatesTimeUnix Timestamps vs ISO 8601: Which to Use in Your API?EncodingUTF-8 Explained: How Computers Store TextTextCORS Explained: Why Your API Call is BlockedTextHTTP Status Codes: A Practical Developer GuideRegexNamed Capture Groups in Regex: Clean ExtractionsColorsColor Spaces Explained: RGB, HSL, HEX, and BeyondColorsColor Contrast for Developers: WCAG Rules and How to CheckNumbersNumber Bases Explained: Binary, Octal, Decimal, and HexNumbersBitwise Operations for Web DevelopersHTMLHTML Semantic Elements: A Complete ReferenceTextText Encoding for Developers: ASCII, UTF-8, and UnicodeRegexWhat is Regex? Complete Guide for DevelopersJSONJSON Format Explained: Structure, Syntax, and Common ErrorsHTMLHow to Convert Any Website to Markdown (for LLMs, RAG & Docs)TextPreparing Website Content for RAG: Clean Markdown Pipelines