JWT Authentication Explained: How JSON Web Tokens Work

JWT Authentication Explained: How JSON Web Tokens Work

If you’ve built or used a modern web API, you’ve almost certainly run into JWT authentication. JSON Web Tokens have become the default way to handle logins in stateless applications, APIs, and single-page apps. But a lot of developers use them without really understanding what’s inside that long string of gibberish — which leads to security mistakes. Let’s fix that with a clear, practical explanation.

What a JWT actually is

A JSON Web Token (JWT, often pronounced “jot”) is a compact, self-contained way to represent claims — pieces of information — between two parties. In plain terms, it’s a signed token that says “this user is who they claim to be,” which a server can verify without looking anything up in a database.

A JWT is just a string made of three parts separated by dots: header.payload.signature. Each part is Base64URL-encoded, which is why it looks like random characters but really isn’t encrypted — anyone can decode and read it. That distinction between encoded and encrypted is the single most important thing to understand about JWTs.

The three parts, decoded

  • Header — metadata about the token, mainly which signing algorithm is used (e.g., HS256 or RS256).
  • Payload — the claims: who the user is (a user ID), when the token was issued, when it expires, and any other data you choose to include.
  • Signature — a cryptographic signature over the header and payload, created with a secret key. This is what makes the token trustworthy.

Want to see this for yourself? You can paste any token into a tool like our JWT decoder to inspect the header and payload instantly — a handy habit when you’re debugging auth.

How JWT authentication works, step by step

The flow is elegantly simple:

  • A user logs in with their credentials.
  • The server verifies them and creates a JWT, signing it with a secret only the server knows.
  • The server sends the token back to the client, which stores it.
  • On every subsequent request, the client sends the token, usually in an Authorization: Bearer <token> header.
  • The server verifies the signature and, if valid, trusts the claims inside — no database lookup required.

Why the signature is everything

Here’s the clever part. Because the token is signed with the server’s secret key, the server can verify it hasn’t been tampered with. If an attacker changes even one character of the payload — say, swapping their user ID for an admin’s — the signature no longer matches, and the server rejects it. The server doesn’t need to remember the token; it just needs to check that the signature is valid. That’s what makes JWTs stateless and so scalable.

The security mistake almost everyone makes at least once

Because the payload is only encoded, not encrypted, never put sensitive data in a JWT. Passwords, credit card numbers, secrets — anyone who intercepts the token can read all of it with a simple decoder. Put only non-sensitive identifiers in the payload, like a user ID and role.

Two more essentials: always set a reasonable expiration (exp) so a stolen token doesn’t work forever, and always transmit tokens over HTTPS so they can’t be sniffed in transit. A long-lived token with no expiry is a liability waiting to happen.

Stateless power, and its trade-off

The great strength of JWTs — that the server doesn’t store them — is also their trickiest weakness. Because the server isn’t tracking tokens, you can’t easily “log someone out” by invalidating a single token; it stays valid until it expires. Teams solve this with short-lived access tokens paired with longer-lived refresh tokens, and sometimes a denylist for revoked tokens. It’s a deliberate trade-off: you gain scalability and simplicity, and in return you plan for revocation explicitly.

When to use JWTs (and when not to)

JWTs shine for stateless APIs, microservices that need to pass identity around, and single-page or mobile apps talking to a backend. For a traditional server-rendered website with classic sessions, old-fashioned session cookies are often simpler and give you easy revocation for free. Use JWTs because they fit your architecture, not just because they’re fashionable.

Access tokens and refresh tokens in practice

Real-world JWT systems almost never use a single long-lived token. The standard pattern pairs two: a short-lived access token (often 15 minutes) that accompanies every API request, and a longer-lived refresh token (days or weeks) whose only job is to obtain new access tokens. When the access token expires, the client silently exchanges the refresh token for a fresh one, and the user never notices.

Why the dance? It sharply limits the damage of a stolen access token — it’s only useful for minutes — while the refresh token, being used rarely and only against one endpoint, can be stored more carefully and revoked server-side. You get the scalability of stateless verification for 99% of requests, and a revocation point where it matters. If you build JWT auth without this pattern, you’ll usually end up either with tokens that live dangerously long or users who get logged out constantly.

Where should the client store the token?

This question starts more arguments than almost any other in web auth. The two common options each have a trade-off. localStorage is simple and immune to CSRF, but any successful XSS attack can read it and steal the token outright. httpOnly cookies can’t be read by JavaScript at all — which neutralizes token theft via XSS — but they’re automatically attached to requests, which reintroduces CSRF concerns that you then mitigate with SameSite attributes and CSRF tokens.

For most applications, the httpOnly cookie with SameSite=Lax or Strict is the safer default, because XSS-based token theft is both more common and more devastating than CSRF in modern apps. Whichever you choose, choose deliberately — “I put it in localStorage because the tutorial did” is how tokens end up stolen.

Common JWT mistakes checklist

Before shipping JWT auth, run down this list — each item is a real-world breach cause:

  • Accepting the none algorithm. Some libraries historically allowed tokens declaring "alg": "none" to skip signature checks entirely. Pin the algorithms you accept.
  • A weak signing secret. HS256 secrets can be brute-forced if they’re short dictionary words. Use a long, random secret — and keep it in an environment variable, never in code.
  • No expiration. A token without exp is a permanent key. Always set one.
  • Trusting claims before verifying the signature. Decode-then-trust is a classic bug; always verify first.
  • Sensitive data in the payload. Anyone with the token can read it. IDs and roles only.

Frequently asked questions

Are JWTs encrypted? Standard JWTs are signed, not encrypted — anyone can read the payload. (An encrypted variant, JWE, exists but is far less common.) Treat every JWT payload as public.

How do I log a user out with JWTs? Delete the token client-side, and rely on short access-token lifetimes for the rest. If you need instant server-side revocation, keep a denylist of revoked tokens or revoke the refresh token — accepting that you’ve traded away a little statelessness.

JWT vs sessions — which should I use? If you have a single server-rendered app, classic sessions are simpler and revocable by default. JWTs earn their keep with APIs, mobile clients, and distributed systems where a shared session store is awkward.

The takeaway

JWT authentication lets a server verify a user’s identity from a signed, self-contained token instead of a database lookup — which is exactly why it powers so many modern APIs. Remember the core truths: the token is signed (trustworthy) but only encoded (readable), so keep secrets out of the payload, always set expirations, and always use HTTPS. Get those fundamentals right and JWTs are a clean, scalable foundation for authentication.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *