Skip to content
← All writing
February 4, 2026·7 min read

API Authentication with JWTs, Explained

Once your app has users, your API needs to know who is making each request — every protected endpoint, every time. The most common answer today is the JSON Web Token (JWT), defined in RFC 7519 and used by practically every auth provider and framework you'll meet. It's everywhere, it's genuinely well-designed for its job — and it's often used slightly wrong, in ways that work fine right up until they're a security incident. This post explains what a JWT actually is, how the authentication flow works, and the handful of security details that separate a safe implementation from a risky one.

The problem: HTTP forgets

HTTP is stateless — each request stands alone, and the server doesn't inherently remember that you logged in a moment ago. So after a user signs in, every subsequent request needs to prove who it's from. The traditional answer was server-side sessions: the server stores "session xyz = user 42" and the client presents the session ID. That works, but it means every request costs a session lookup, and every server handling requests needs access to the session store.

A JWT is the other approach: instead of the server remembering who you are, you carry a tamper-proof note saying who you are, and the server just checks the note is genuine. No lookup, no shared session store — which is exactly why the pattern took over in the era of multiple services and horizontal scaling.

What a JWT actually is

A JWT is a string with three parts separated by dots — xxxxx.yyyyy.zzzzz — a header, a payload, and a signature, each base64-encoded.

  • The header says which signing algorithm was used.
  • The payload holds "claims" — small facts like the user's ID (sub), when the token expires (exp), and who issued it.
  • The signature is the important part. The server creates it from the header and payload using a secret key. Anyone can read the token, but only a holder of the key can produce a valid signature — so any tampering with the claims breaks the signature and is detected instantly. (You can paste any JWT into jwt.io and inspect it — a useful debugging trick and a vivid demonstration of the next point.)

That next point trips people up constantly: a JWT is signed, not encrypted. The payload is merely encoded, not hidden — anyone who obtains the token can read every claim in it with one line of code. Signing guarantees integrity (nobody altered it), not confidentiality (nobody read it). So never put sensitive data — passwords, personal details, anything you wouldn't show the user — in a JWT payload. The token proves identity; it isn't a secure envelope.

The authentication flow

  1. Login. The user sends credentials (or an OAuth provider's token). The server verifies them and creates a signed JWT containing the user's ID and a short expiry.
  2. Store. The app keeps the token — in secure storage on mobile, never plain preferences.
  3. Send. On each request to a protected endpoint, the app includes the token, conventionally in an Authorization: Bearer <token> header.
  4. Verify. The server checks the signature and expiry. If valid, it trusts the claims and knows who's calling — without touching the database.

That last step is the appeal: authentication becomes a fast, local cryptographic check on whatever server received the request. It's also the trade-off to keep in mind — the server trusts the token because it can't check anything else, which is why expiry and revocation (below) deserve real thought.

Access tokens and refresh tokens

Here's the tension: short-lived tokens are safer (a stolen one expires quickly), but forcing users to log in every hour is miserable. The standard resolution is two tokens with different jobs:

  • A short-lived access token (minutes to an hour) sent with every request — the workhorse.
  • A longer-lived refresh token used only at the token endpoint, to obtain a fresh access token when the old one expires.

The app quietly refreshes in the background — intercept the 401, refresh, retry, and the user never notices — so sessions feel permanent while any leaked access token is only briefly useful. Refresh tokens are the more sensitive credential: store them carefully, make them revocable server-side (they're long-lived, so the database lookup on refresh is acceptable — it happens once an hour, not once a request), and ideally rotate them on each use so a stolen-and-replayed refresh token becomes detectable. This split — stateless verification for the frequent operation, stateful control for the rare one — is the design that gives you most of both worlds.

The security details that matter

JWTs are safe only if you handle the sharp edges deliberately:

  • Always use HTTPS. A bearer token is exactly what it sounds like — whoever bears it, is you. Over plain HTTP it can be read and replayed by anyone on the path. Non-negotiable.
  • Keep expiry short. A standard JWT can't be "un-issued" — it's valid until it expires, wherever it is, whoever holds it. Short access-token lifetimes are your main damage-limitation tool.
  • Store tokens securely on the device. Keychain/Keystore on mobile, with the same care as any credential.
  • Protect the signing secret above all. Anyone holding it can mint valid tokens for any user — it's the master key to your entire API. Environment variables or a secret manager, never the repo, rotated if ever exposed.
  • Verify properly, every time. Use a maintained JWT library, verify the signature and the expected algorithm, and reject alg: "none" tokens — the classic attack, called out in RFC 8725 (JWT Best Current Practices), where an attacker crafts an "unsigned" token and a naive verifier accepts it. Validate the standard claims too: expiry, issuer, audience.
  • Plan for revocation before you need it. "Log out everywhere," "ban this account now," "we had a breach, invalidate everything" — pure stateless JWTs can't do these mid-lifetime. The practical pattern: short access tokens plus revocable refresh tokens covers most needs; a small denylist of revoked token IDs, checked on sensitive endpoints, covers the rest. Decide this on a calm day, not during the incident.

Don't over-reach

JWTs are for authentication — proving who's calling. Resist the temptation to stuff them with roles, preferences, and feature flags until they're a 4KB cache of the user record: every byte rides on every request, and every claim is frozen at issue time — a "role: admin" claim revoked in the database keeps working from the token until it expires. Keep the payload minimal — an ID, an expiry, maybe a coarse role — and look up the rest fresh when it matters. For authorization decisions that must be current (is this user still allowed to do this?), check the database; the JWT answers "who," not "may they, right now."

Summary

A JWT is a signed, tamper-evident token that lets your API verify who's making a request with a local cryptographic check instead of a session lookup — readable by anyone, forgeable by no one without your key. Use short-lived access tokens with revocable, rotating refresh tokens; run everything over HTTPS; store tokens in secure storage; guard the signing secret like the master key it is; verify with a real library per RFC 8725; and keep payloads small, non-sensitive, and authenticated-not-authorized. Handled with those details in mind, JWTs are a clean, scalable foundation for app auth — boring in precisely the way security should be.

Further reading