JWT Authentication Explained: How Token-Based Login Actually Works
Building the JWT decoder tool meant handling every malformed token edge case I could find — this article covers what that process taught me about how JWTs actually fail in practice.
If you've built or used any modern web application, there's a good chance JWT (JSON Web Token) authentication is working behind the scenes. It's become the default choice for API authentication, single-page applications, and mobile apps — replacing traditional server-side sessions in many architectures. Understanding how JWTs actually work — not just that they exist — helps you build more secure systems and debug authentication issues faster.
The Problem JWTs Solve
Traditional web authentication relied on server-side sessions: when a user logs in, the server creates a session record in memory or a database and sends the browser a session ID cookie. Every subsequent request includes that cookie, and the server looks up the session to verify identity. This works well for a single server, but becomes complicated in modern distributed systems — if you have multiple API servers behind a load balancer, every server needs access to the same session store, adding infrastructure complexity and a single point of failure.
JWTs solve this by making authentication stateless. Instead of the server storing session data, the token itself contains the claims (user ID, roles, expiry) and is cryptographically signed. Any server holding the shared secret (or public key, for asymmetric signing) can verify the token's authenticity without needing to check a central database — the token carries its own proof of validity.
Anatomy of a JWT
A JWT consists of three parts separated by dots: header.payload.signature. Each part is base64url-encoded (not encrypted — this is a critical distinction covered below). The header specifies the token type and the signing algorithm, typically something like {"alg":"HS256","typ":"JWT"}. The payload contains the claims — data about the user and the token itself, such as {"sub":"12345","name":"John","exp":1735689600}. The signature is generated by taking the encoded header and payload, combining them with a secret key, and running them through the algorithm specified in the header (commonly HMAC-SHA256 or RSA).
Encoded, Not Encrypted — A Critical Distinction
This is the single most misunderstood aspect of JWTs. Base64url encoding is not encryption — it is trivially reversible by anyone, requiring no secret key. Anyone who intercepts a JWT can decode the header and payload instantly and read every claim inside. The signature does not hide the data; it only proves the data hasn't been tampered with since it was signed. Never store sensitive information (passwords, credit card numbers, private personal data) directly in a JWT payload — treat it as readable, not confidential.
Standard Claims
The JWT specification (RFC 7519) defines several standard claim names, though none are strictly required: sub (subject — usually the user ID), iss (issuer — who created the token), aud (audience — who the token is intended for), exp (expiration time, as a Unix timestamp — the token must be rejected after this time), iat (issued-at time), and nbf (not-before — the token isn't valid until this time). Applications commonly add custom claims like user roles, permissions, or tenant IDs specific to their domain.
Access Tokens vs Refresh Tokens
Most production systems use two tokens working together. The access token is short-lived (typically 15 minutes to 1 hour) and sent with every API request to prove identity. The refresh token is long-lived (days to weeks) and stored more securely (often in an httpOnly cookie, inaccessible to JavaScript) — its only job is to request a new access token when the old one expires, without forcing the user to log in again. This pattern limits the damage if an access token is stolen (it expires quickly) while still providing a smooth user experience.
Common JWT Security Pitfalls
- Storing JWTs in localStorage: accessible to any JavaScript running on the page, making them vulnerable to XSS (cross-site scripting) attacks. HttpOnly cookies are generally safer for token storage.
- Not validating the algorithm: some libraries had vulnerabilities where an attacker could change the header's "alg" field to "none" and bypass signature verification entirely. Always use a well-maintained JWT library and explicitly specify allowed algorithms.
- No expiry, or extremely long expiry: a stolen token with no expiration is valid forever. Always set a reasonable exp claim.
- Weak signing secrets: a short or guessable HMAC secret can be brute-forced, allowing an attacker to forge valid tokens. Use a cryptographically random secret of at least 256 bits.
- No revocation mechanism: because JWTs are stateless, there's no built-in way to invalidate a token before its expiry (e.g., on logout or password change) without maintaining some server-side denylist, which reintroduces the statefulness JWTs were meant to avoid. Many systems solve this with short access token lifetimes plus a revocable refresh token.
Debugging JWTs
When an API call fails with an authentication error, the fastest first step is decoding the token to check its claims — is it actually expired, does it contain the expected user ID, was it issued for the right audience? Rather than manually base64-decoding each segment, a tool like our JWT Decoder instantly shows the header, payload, and an expiry countdown, making it far faster to spot the issue — whether that's an expired token, a missing claim, or a malformed structure — without writing throwaway debug code.
JWTs in Practice: A Login Flow
A typical JWT-based login flow: the user submits credentials to a login endpoint; the server verifies them against the database and, on success, generates a signed JWT containing the user ID and role, then returns it to the client. The client stores this token and includes it in the Authorization: Bearer <token> header on every subsequent API request. Each API server, without querying any database, verifies the signature using the shared secret, checks the expiry claim, and — if valid — trusts the claims inside as the authenticated user's identity. This entire verification happens in microseconds with no network round-trip, which is the core performance advantage over traditional session lookups.
Try Our Free Tools
Put what you just learned into practice with HukhLatri's free online tools.
Explore All Tools →