Skip to content
On this page

JWT authentication architecture

5 min read

What happens under the hood when your app uses JWT: the full walk from login to logout, one stage at a time, with every piece explained.

Have you ever wondered what actually happens under the hood when you sign in to an application and it "remembers" you on every screen? Today we're going to walk the whole JWT authentication flow: from the moment you type your email and password to the moment you hit log out, seeing at each step who does what, what gets stored and what doesn't.

We'll use Lumen, a fictional store, as our example, and we'll take it slowly: for each stage of the flow I'll tell you what happens first, and then you'll see it animated.

Before walking the flow, take a look at the thing we're going to talk about. A JWT (JSON Web Token) is a string with three parts separated by dots: the header says which algorithm signed it, the payload carries the data —who you are, how long it's good for— and the signature is what lets anyone check that nobody tampered with them. The first two parts are encoded text, not encrypted, so they can be read. Edit it, or paste one of your own:

A JWT from the inside

..

header

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

payload

{
  "sub": "u_42",
  "exp": 1787480100
}

Expires on Aug 23, 2026, 10:15 AM UTC

signature

Bytes, not text. Without the server's secret they say nothing, which is why this part isn't decoded.

Open a part to see it decoded.

That's the whole idea, and it's worth fixing in your head before we go on: instead of the server remembering who you are, you're the one carrying the proof. And that proof isn't secret — as you just saw, anyone holding it can read what's inside.

It all starts at login

When you log in to the application you send your credentials, which are usually an email and a password. That's the only time in the entire flow that the password travels over the network, and that's why it's the one moment that absolutely has to go over HTTPS.

The server receives those credentials and does three things, in this order:

  1. It verifies who you are. It looks up the user and compares the incoming password against the stored hash — it never keeps the password in the clear.
  2. It builds the token's payload. A small object holding who you are, in the sub claim, and how long the token is good for, in the exp claim. A claim is each of the fields that go inside the token, and those two short names come from the standard.
  3. It signs the payload with its secret and hands the token back to you.
Loading the animation…

Notice how the flow ends: the server signs the token, hands it over and keeps no copy at all. There's no sessions table, no row pointing at you. That decision is what explains everything that follows, both the good parts and the awkward ones.

Every request carries the token

Now you have the token. From here on, every request the application makes sends it in the Authorization header, in the form Bearer <token>. "Bearer" means exactly that — the holder: it's good because you have it, like a train ticket with no name on it.

On the server side, validation is three checks and not one lookup. The token arrives as three parts separated by dots —header, payload and signature— so first it splits it. Then it recomputes the signature with its secret and compares it against the one the token carries: if anyone changed even a single character of the payload, the two signatures don't match and the request is rejected with a 401. And finally it checks exp, the expiry date, against its clock:

Loading the animation…

The interesting thing about this stage is what's missing: at no point was the database queried. The server doesn't need to know anything about you that isn't written in the token, which is why any service holding the secret can validate on its own, without talking to whoever issued it.

The token expires, and that's where refresh comes in

Since the token is good simply because you hold it, anyone who steals it gets in as you. The defence is to make it short-lived: fifteen minutes is a common value. But that raises the obvious question — nobody is going to type their password every fifteen minutes.

This is where the architecture goes from one token to two, each with a different job:

  • The access token is the one you just saw: short-lived, signed, stateless, sent on every request.
  • The refresh token is a long random string, single-use, which is stored in a table on the server and which on the client lives in an httpOnly cookie —a cookie the page's JavaScript cannot read—. Its only job is to get you a new access token.

When the access token expires, the application doesn't send you back to the login screen: it calls the refresh endpoint, the server looks the refresh token up in its table, checks that it's still valid, issues a new pair and marks the old one as used:

Loading the animation…

That "marks the old one as used" is the part a lot of people skip, and it matters: if a refresh token that was already redeemed shows up again, either the client has a bug or somebody is using a stolen copy. The answer is the same in both cases — invalidate that session's entire chain of tokens and force a fresh login.

Logging out, and what logging out can't do

We've reached the end of the walk. Logging out moves two things: the client drops its access token, and the server deletes the refresh token's row, so that session can never be renewed again.

But there's a third thing that doesn't move, and this is where the architecture shows its trade-off. If a copy of the access token exists somewhere —in a log, in a proxy, in somebody's clipboard— that copy still carries a correct signature and an exp that hasn't arrived yet. And as you just saw, the server validates without looking anything up:

Loading the animation…

In other words: logging out is complete on the refresh token side and incomplete on the access token side. You cut off renewal instantly and you leave a window as long as whatever life the token has left, which with a fifteen-minute expiry is fifteen minutes at most. You can close that window by storing the identifier of revoked tokens and checking that list on every request, but that hands the server back exactly the state this design set out to avoid.

The whole flow at a glance

Recapping the entire walk:

MomentWhat the client sendsWhat the server doesDoes it query the database?
Loginemail and passwordverifies and signs the token pairyes
Normal requestaccess tokenchecks the signature and expno
Refreshrefresh tokenlooks it up, rotates it, issues a new pairyes
Logoutrefresh tokendeletes the rowyes

That right-hand column is, deep down, what this whole architecture is about: the normal request —the one that happens thousands of times— never touches the database, and that's the reason the pattern exists at all. Everything else is a consequence of having bought that property.

If you want to see it in your own project, open the dev tools on the network tab, sign in to your application and look at the Authorization header of any request: that's the token we've been talking about for this entire post.