DEV Community

Cover image for Demystifying JWTs: What Actually Happens Under the Hood published: true tags: webdev, security, javascript, architecture
CHRISTIAN OTIENO
CHRISTIAN OTIENO

Posted on

Demystifying JWTs: What Actually Happens Under the Hood published: true tags: webdev, security, javascript, architecture

JSON Web Tokens (JWTs) are practically everywhere. We reach for them when building microservices, mobile APIs, and single-page apps.

Yet when asked how they actually work under the hood, the standard developer answer usually defaults to:

"It's a signed string you store in localStorage and pass along in an Authorization header."

Let’s dismantle that abstraction. A JWT is not encrypted by default, it is rarely magic, and implementing it blindly introduces real security headaches.

Here is what is actually going on under the hood.

The Anatomy of a JWT
A JWT is fundamentally a single string divided into three distinct segments separated by dots (.):

_

Plaintext

header.payload.signature
_
Every segment is encoded using Base64Url—not encrypted. Anyone who intercepts the token can paste it into an online decoder and inspect the data instantly.

Let’s break down the three parts.

  1. The Header The header contains metadata about the token: the type and the cryptographic algorithm used to secure it.

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

  1. alg: The signing algorithm (e.g., symmetric algorithms like HS256, or asymmetric key pairs like RS256).
  2. typ: Almost universally set to "JWT".

  3. The Payload
    The payload contains the claims—statements about the user and context the server needs to function statelessly.

JSON
{
"sub": "usr_94810284",
"name": "Jane Doe",
"role": "admin",
"iat": 1711926000,
"exp": 1711929600
}
Claims fall into two primary buckets:

Registered claims: Standardized keys like sub (subject/user ID), iat (issued-at timestamp), and exp (expiration timestamp).

Custom claims: Application-specific context like role, tenant_id, or access scopes.

⚠️ Critical Security Rule: Never store sensitive data (passwords, private API keys, or unencrypted PII) inside a JWT payload. Encoding is not encryption.

  1. The Signature This is the integrity anchor of the entire token. The signature prevents tampering.

To generate it, the issuing server takes the Base64Url-encoded header, appends the Base64Url-encoded payload, and hashes them using a private secret:

JavaScript
const signature = HMACSHA256(
${base64UrlEncode(header)}.${base64UrlEncode(payload)},
SERVER_SECRET_KEY
);
When a client sends the token back, the server re-runs that exact calculation. If the newly calculated signature matches the signature attached to the token, two things are guaranteed:

Integrity: The header and payload were not altered in transit.

Authenticity: The token was issued by an entity holding the secret key.

The Lifecycle of a Stateless Request
Here is the complete authentication flow in practice:

[ Client ] [ Server ]
| |
|---- 1. POST /api/login (Credentials) ------------->|
| |-- Validates credentials
| |-- Signs JWT with secret
|<--- 2. Returns JWT (Set-Cookie / Body) ------------|
| |
|---- 3. GET /api/dashboard (Bearer ) ------->|
| |-- Recomputes signature
| |-- Checks exp timestamp
| |-- Serves data (Zero DB lookups!)
|<--- 4. HTTP 200 OK --------------------------------|
Authentication: The client submits credentials to /api/login.

Issue: The server checks credentials against the database, builds the claims payload, signs it with its secret, and returns the token.

Storage: The client stores the token (preferably in an httpOnly, Secure, SameSite cookie to mitigate XSS attack vectors).

Transport: Subsequent calls pass the token via the Authorization header:

HTTP
Authorization: Bearer eyJhbGciOi...
Stateless Verification: The server recomputes the signature using its secret and checks the exp timestamp. If valid, the request proceeds—without querying a session database.

Architectural Trade-offs: When NOT to Use Pure JWTs
Statelessness makes horizontal scaling easier, but it introduces tradeoffs that standard tutorials gloss over:

Feature Session IDs (Stateful) JWTs (Stateless)
Revocation Instant (delete record from Redis/DB). Difficult (valid until exp triggers).
Payload Size Tiny (~32-character pointer string). Larger (carries claims on every request).
DB / Cache Load Hit on every authenticated request. Zero DB lookups for validation.
Server Sync Requires shared session store. Any service with the key/public key can verify.
The Revocation Problem
If a user changes their password, reports a compromised account, or gets suspended, an issued JWT remains completely valid until its exp time passes.

To fix this in production, architectures commonly use:

Short-lived access tokens (5–15 minutes) combined with stateful refresh tokens stored securely in a database.

Revocation blocklists in Redis (which sacrifices pure statelessness).

Core Takeaways
Base64Url is encoding, not encryption. Don't hide secrets in the payload.

Signatures ensure integrity, not secrecy. They only prove the data hasn't been modified.

Statelessness is a double-edged sword. Build an explicit revocation strategy before relying on JWTs in production.

Over to you: How does your team handle token invalidation? Are you pairing short-lived access tokens with refresh tokens, or relying on centralized session stores like Redis? Let's discuss in the comments below!

Top comments (0)