DevTools Hub

Search tools

Search for a developer tool

What Is JWT Authentication?

Part of the JWT Toolkit

"JWT authentication" gets used loosely to mean almost any login system that hands back a token instead of a session cookie. The token format itself is covered in What Is a JWT? A Practical Guide — this post is about the part that actually matters day to day: the flow around that token, from login to every request after it.

The flow, step by step

  1. The client sends credentials (username/password, or a social login callback) to a login endpoint.
  2. The server verifies them and, if valid, signs a JWT containing claims like sub (the user ID) and exp (expiration), and returns it.
  3. The client stores the token somewhere it can retrieve it for future requests.
  4. On every subsequent request, the client attaches it — almost always as Authorization: Bearer <token>, per RFC 6750.
  5. The server verifies the signature and checks claims like exp and aud — no database lookup required, which is the entire point of using a JWT here instead of a session ID.

That last step is what makes JWT auth attractive for APIs and microservices: any service holding the verification key can authenticate a request on its own, without calling back to a central session store.

Access tokens and refresh tokens

A JWT that's valid for hours or days is a standing liability if it leaks — there's no way to force it to expire early short of maintaining a revocation list, which defeats the statelessness you wanted in the first place. The common fix is splitting the token into two:

  • A short-lived access token (often 5–15 minutes) — the JWT that actually gets sent with API requests. If it's stolen, the window of usefulness is small.
  • A long-lived refresh token — used only to request a new access token from a dedicated endpoint when the old one expires. It's presented far less often, which shrinks its exposure, and it's frequently opaque (a random string checked against a database) rather than a JWT, precisely so it can be revoked or rotated server-side.

A refresh done well also rotates the refresh token: each use issues a new one and invalidates the old, so a stolen-but-unused refresh token can be detected the moment the legitimate client tries to use its now-invalid copy.

Where should the token live?

For a browser-based client, the two realistic options each carry a different risk:

  • localStorage / memory — readable only by your own JavaScript, so it's not automatically sent on cross-site requests (no CSRF risk). But any script that manages to run on your page via XSS can read it directly and exfiltrate it.
  • An httpOnly cookie — invisible to JavaScript entirely, so an XSS bug can't read the token itself. But the browser attaches cookies automatically, so you're back to needing CSRF protection (SameSite=Strict or Lax, plus a CSRF token for state-changing requests if you need cross-site form posts to work).

Neither option is risk-free; they trade one class of attack for another. Native mobile apps sidestep most of this by using the platform's secure storage (Keychain, Keystore) instead of anything a webview's JavaScript can reach.

What the server checks on every request

Verifying a JWT is more than confirming the signature is valid — that only proves the token wasn't tampered with, not that it's still usable. A correct check also confirms:

  • exp hasn't passed, and nbf (if present) has.
  • aud matches this service, if the token could be used for more than one audience.
  • iss matches the auth server you actually trust, if you accept tokens from more than one issuer.

That's authentication — confirming who's making the request. Authorization — what they're allowed to do — is a separate check layered on top, usually against a roles or scope claim in the payload. Mixing the two up, and treating "the token is valid" as "the user can do this," is a common source of access-control bugs. The deeper security implications of all this — algorithm confusion, alg: none, key management — are covered separately in JWT Security Best Practices.

JWT auth vs. session-based auth

Session auth stores a random session ID in a cookie and keeps everything else — user ID, roles, whatever — server-side in a database or cache. Every request means a lookup, but that also means revocation is instant: delete the session row and the user is logged out everywhere, immediately. JWT auth inverts that trade — no lookup needed to verify a request, but no instant revocation either, only the short-access-token-plus-refresh pattern above to bound the damage.

Neither is strictly better. Session auth fits a single monolithic backend well; JWT auth fits distributed services that need to verify requests without a shared session store in the loop.

JWT authentication isn't OAuth2

It's worth untangling these, since they're constantly conflated. OAuth2 (RFC 6749) is an authorization framework — it defines flows for one service to get a scoped token to act on a user's behalf against another service, and its access tokens don't have to be JWTs at all; plenty of OAuth2 deployments use opaque tokens instead. OpenID Connect layers identity on top of OAuth2 and does standardize its ID token as a JWT. A typical home-grown "log in, get a JWT" system most people mean by "JWT authentication" is neither of these — it's just using the JWT format as a convenient, self-verifying session replacement, with none of OAuth2's delegation or third-party-authorization machinery involved.

Common flow mistakes

  • No refresh strategy at all — issuing a single long-lived JWT because implementing refresh felt like extra work, leaving no way to shorten a stolen token's useful life.
  • Storing the refresh token the same way as the access token. If both sit in localStorage, an XSS bug that steals one steals both, defeating the entire point of separating them.
  • No logout-time invalidation plan. "Logging out" that only deletes the client-side copy leaves the token — and any refresh token — valid until it naturally expires if someone captured it beforehand.
  • Trusting claims for access control without re-verifying server-side on every service that consumes the token, not just the one that issued it.

Try it yourself

Build a test token with our JWT Generator, then check what a request would actually see with JWT Decoder or confirm its signature is valid with JWT Signature Verifier. All three run entirely in your browser — nothing you paste is sent anywhere.

Related tools