Dickson
BlogAboutGuestbook
BlogAboutGuestbook

© 2026 Dickson Boateng

EmailGitHubLinkedIn
Back to blog

How JWT Authentication Works in Node.js

July 29, 2026•16 min read

Every time you visit a website or search for something online, something very simple happens behind the scenes: your browser sends a request to a server, asking for that thing you just searched (a website, article, image, etc.). The server then searches for what you requested, finds it, and sends it right back to your browser.

That's really it. So when you type the URL of a website and search for it, you're basically telling your browser to write a request to the server saying "hey, can I have the homepage of this website?" The server reads that request, searches for what you asked for, and sends back a response. Your browser opens the response and shows you the page.

Live demo
yoursite.com
Waiting...
Server

Click to see it in action

This back-and-forth is happening constantly, every time you type to search for something in a browser, click a link, submit a form, etc.

Literally every action you take on a website or web app: clicking a button, scrolling to load more posts, submitting a form, liking a comment, is a separate request that is sent to the server. Using a particular site might feel like one continuous experience, but underneath, it's dozens or even hundreds of these request-response exchanges happening one after another.

Now, here's the part of this browser-server communication that confuses people: each request is treated as brand-new. The server is stateless, meaning it has no memory of the last request it received from you. It doesn't recognise you; it doesn't even recall what you asked for five seconds ago unless something is specifically built to make it remember.

YoubrowserServerrequest 1response 1request 2?no memory of request 1Same browser, same server — but request 2 is treated as a total stranger

This is a very strange behaviour, if you think about it. Imagine walking into a shop and asking to buy something, and every time you speak, even ten seconds later, the shopkeeper treats you like a complete stranger who just walked into the shop for the first time. But that's the default behaviour of how the web runs.

So this raises an obvious question: if the server forgets you the moment a request is completed, how does it then know you're still logged in when you're using a website or an application?

That question is the whole reason JWTs exist.

What is JWT?

JWT stands for JSON Web Token.

From here on, we'll use "JWT" and "token" interchangeably. They mean the same thing throughout this article, so don't worry if I switch between the two.

Remember the problem we ended on: the server forgets you the second a request is completed. So somehow, we need to give the server a way to recognise you on your next requests. JWT is one answer to that problem. But to really understand why it's designed the way it is, it helps to look at how this problem was traditionally handled, and the limitations that came with it.

The traditional method: making the server keep records

The earliest common solution to this was sessions.

Here's how it worked: when you logged into a site, the server would create a little record about you, something like "this is Jermaine, he logged in at 10:03am, he's authenticated," and store that record in its own memory or database. It would then hand you a small ID, called a session ID, and your browser would attach that ID to every request you made afterward.

So this is what the conversation looks like when you refresh the page or navigate elsewhere in the app: "Hey server, it's me again, session ID 8842." The server checks its records, finds the one for 8842, sees that it's you, and lets you carry on as if you'd never left.

This worked, and it's still used today. But notice what it requires: the server has to keep a record for every logged-in user in memory or a database, and it has to search for that record on every single request.

That's fine with a handful of users. But think about an app like Instagram or Twitter. Millions of people are logged in at once, all sending requests every second: scrolling, posting, liking. One server, no matter how powerful, can't handle that much traffic alone. So companies run many servers instead, and spread incoming requests across them.

Youthe clientLoad balancerpicks a serverServer Arecords: 8842 ✓recognizes youServer Brecords: nonedoesn't know yourequest 1 (login)request 2 (later)Same client, different request. One server remembers you, the other doesn't.

A load balancer sits between you and those servers. It decides which server handles each request, usually just to spread traffic out evenly, with no regard for which server handled your last one.

Here's the problem that creates: say you log in and your session record gets created on Server A. A few seconds later you click something else, but this request happens to land on Server B. Server B checks its own records and finds nothing. It has no idea who you are, because your record only exists on Server A.

To fix that, every server would need to share the exact same set of records, and stay perfectly updated with each other at all times. The instant one server creates or changes a record, every other server needs to know about it too. That's genuinely difficult to build and maintain very well. It's a real headache for software engineers, and it's the exact kind of problem that got developers asking: is there a better way to do this without needing every server to constantly match what every other one knows?

The new method: letting the client hold its own record

This is where JWT comes in.

Instead of the server creating a record and keeping it in its own memory or database, JWT flips the whole idea around: the server writes the entire record, who you are, when you logged in, whatever else it needs to know about you, directly onto a token, then stamps it with a special signature only the server knows how to make, and hands the whole thing back to your browser.

The signature doesn't hide anything on the token. Anyone who gets hold of it can still read what's written there. What it does instead is prove two things:

  • That the token really came from the server
  • And that nobody has changed it since. If someone tries to alter even one letter on the token, the signature breaks, and the server immediately knows something is wrong.

Your browser holds onto that token and attaches it to every request you send from then on. The server reads straight off the token instead of searching for it.

So the conversation now looks like this: "Hey server, it's me, here's my token." The server just checks the signature to confirm the token is genuine and untampered with, then reads who you are directly from it. That's it.

Notice what just disappeared: there's no shared record store anymore. No Server A creating something Server B has never seen. No syncing, no real-time updates between servers. Each server can verify your token completely on its own, the moment it receives it.

Youholds a tokenLoad balancerpicks a serverServer Achecks signature ✓recognizes youServer Bchecks signature ✓recognizes you toorequest 1 (token attached)request 2 (same token)Same client, different server, same result. No shared record needed.

This is what people mean when they call JWT "stateless." The server goes back to genuinely forgetting you the second a request ends, and that's completely fine, because you're the one holding proof of who you are, every single time.

In short: JWT is a signed token that holds everything a server needs to know about you. Your browser stores it and sends it along with every request you make. Because the information lives on the token itself, any server can verify who you are just by checking the signature. No records to search, no other servers to check in with.

The anatomy of a token

Before we look at how a token gets created and verified, it helps to know what one is actually like.

If you've ever seen a JWT before, you might have noticed it just looks like a long string of letters and numbers. But this isn't random; it's made up of three distinct parts, and once you know what each part does, the whole thing stops feeling like magic.

Here's what an actual JWT looks like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZW1haWwiOiJqZXJtYWluZUBleGFtcGxlLmNvbSJ9.4f7d8e2a1b9c...

At a glance, you'll notice it's really just one long string with two dots in it. Those two dots split the string into exactly three sections: the header, the payload, and the signature.

eyJhbGciOiJIUzI1NiIs...eyJ1c2VySWQiOjEyMywi...4f7d8e2a1b9c...HeaderSigning method usedPayloadThe actual claimsSignatureProves it's untamperedThe dots just join the three parts together.

1. The header

This part carries a small amount of information about the token itself. Mainly, what kind of token it is, and which signing algorithm was used to create the signature (something like HMAC-SHA256, often shortened to HS256).

2. The payload

This is the part we’ve already been talking about, the actual record about you. Your user ID, your email, maybe your role, when the token was issued, when it expires. Whatever the server decided to include when it built the token.

Here’s the part that trips people up the first time they hear it: the payload is not encrypted; it’s just encoded. This means that anyone who gets hold of the token can decode the payload and read exactly what’s on it, in plain text. Try it yourself: paste any JWT into a site like jwt.io, and you’ll see the payload displayed as plain, readable JSON.

This is exactly why the golden rule of JWTs is: never put sensitive information in the payload. No passwords, no card numbers, nothing you wouldn't be comfortable with a stranger reading.

3. The signature

This is where the “proof” we talked about earlier actually lives. The signature is created by taking the header and payload, running them through the signing algorithm named in the header, and combining that with a secret key that only the server knows.

The result is a signature that changes completely if even one character in the header or payload changes. So when the server later receives this token back, it doesn't need to "remember" you. It just recomputes the signature using its secret key and checks whether it matches the one on the token. If it matches, the token is genuine and untouched. If it doesn’t, the server knows immediately that something’s wrong. Either the token was tampered with, or it wasn’t issued by this server at all.

So, the header says what kind of token this is, the payload says what this token knows about you, and the signature says you can trust that the first two haven’t been messed with.

That’s the entire anatomy of a JWT.

The login flow – issuing a token

So what actually happens the moment you type in your email and password and hit "log in"?

It starts the same way any request does: your browser sends your credentials to the server, and the server checks that email and password against whatever it has stored in a database, to make sure they're correct and belong to a real account.

If they don't match, the server sends back an error, and that's the end of it.

But if they do match, the server still won't remember you on your next request. So instead of trying to remember, it hands you something you can carry forward as proof: a token.

Building that token means putting together the payload, your user ID, maybe your role or email, when the token should expire, then signing it like you saw in the last section.

Once the token is created and handed back to your browser, your browser holds onto it and attaches it to every request it makes from then on.

YoubrowserServerSends email + password1. Verify credentials2. Build payload3. Sign tokenReturns signed token

The server never needs to check a database or "remember" you again. It just checks the token you hand it, every time.

The request flow - verifying a token

So you've logged in, and your browser is now holding onto a JWT. What happens on every request after that?

From here on, your browser attaches the token to every request it sends, usually inside something called the Authorization header.

Before handling anything else about the request, the server checks whether the token is genuine. This check usually happens in middleware, a small piece of code that runs on every incoming request before it reaches its destination.

That middleware pulls the token out of the request and recomputes its signature using the server's secret key, then compares it to the signature already on the token.

Youholds a tokenServermiddlewareRequest + token attachedRecompute signatureMatches ✓reads payload, continuesNo match ✕rejected immediatelyRequest allowed or rejectedNo database lookup. Just a signature check, every single time.

If they match, the server knows the token is genuine and untampered, and reads who you are straight off the payload. If they don't match, or the token's missing or expired, the request gets rejected immediately, and nothing else runs.

Access tokens vs refresh tokens

By now, every time we’ve said “the token,” we actually meant a specific kind of token called an access token. It’s the one your browser has been carrying around and attaching to every request, and it’s what the server checks to know who you are.

Here’s something you should know: a JWT isn’t meant to last forever. When the server first builds the token, it also decides how long that token should remain valid. It can be an hour, a day, whatever the app is comfortable with. Once that time is up, the token stops working entirely. If you try to use it after that point, the server will look at it, see that its time is up, and reject it.

So why would a token ever expire at all? Wouldn’t it be more convenient if, once you logged in, your token just worked forever?

The answer comes down to risk. A token is really just proof and if someone else ever gets hold of your token (say, through a stolen device, or hacking), they can use it to act as you, for as long as that token remains valid. Giving a token a short lifespan say, fifteen minutes, or an hour limits how much damage a stolen token can do. Once it expires, it’s worthless.

However, this creates an obvious problem: if your token expires every hour, does that mean you have to log in again, every single hour? That would be a miserable experience for anyone using the app.

This is where a second kind of token comes in: the refresh token.

A refresh token is longer-lived than an access token. It can sometimes last for days or even weeks. Its only job is to get you a brand-new access token, without you needing to type in your email and password again. It’s usually stored more carefully than the access token too, often in a place ordinary JavaScript on the page can’t easily reach, to reduce the risk of it being stolen.

So here’s how the two work together: when you first log in, the server hands you both an access token and a refresh token.

Access tokenactive, e.g. 1 hourexpiresRefresh token usedquietly, behind the scenesNew access tokenactive againRefresh token stays valid much longer, days or weeksYou never notice the swap. Only a real login issues a new refresh token.

You use the access token as normal, on every request, until it eventually expires. When that happens, instead of logging in again, your browser quietly sends the refresh token to the server, which checks that refresh token to see if it’s valid and issue a brand-new access token.

A refresh token is only ever handed out by the server at two moments:

  • When you log in with your real email and password
  • Or when a valid refresh token is used to request a new access token.

So an attacker who only has your access token can act as you for a while, but the moment that token expires, they’re locked out for good. They will never be given a refresh token by the server, because they don’t have your email and password to get one.

This gives you the best of both worlds: an access token that's short-lived enough to limit damage if it's ever stolen, and a refresh token that keeps you logged in for a long time without repeatedly asking for your credentials.

The hard part: revocation and storage

Everything so far has made JWTs sound almost too good to be true.
No database lookups, no shared memory between servers, just a signature the server can check on its own. But there’s a real tradeoff hiding underneath all of that .

The problem: you can’t take a JWT back

Think about what “logging someone out” actually means with sessions (the old approach). The server just deletes that person’s record from its database (recommend an appropriate word here). The next request they send has nothing to match against, so the server no longer recognises them.

Now think about what “logging out” actually does with a JWT. Clicking “log out” here does not make your access token stop working. It can’t; the server never stored that token anywhere in the first place, so there’s nothing to go delete. As long as an access token hasn’t expired yet, and its signature still checks out, the server has no way of knowing it’s “supposed” to reject it.

So what does logging out actually do, then? Two things:

  • Your browser deletes its own copy of the access and refresh tokens, so it stops sending them on future requests.
  • And separately, the server revokes the refresh token, since that's the one piece of this the server actually keeps track of.

But notice what this doesn’t cover. If a copy of your access token already exists somewhere else, let’s say, it was stolen five minutes ago, logging out on your own device does nothing to that stolen copy. It doesn’t know a theft happened. It just clears your own browser and revokes the refresh token. The attacker’s copy of the access token keeps working, completely unaffected, for as long as that token remains valid.

This is exactly why keeping access tokens short-lived matters so much. It’s not only about limiting general damage from a stolen token — it specifically covers this exact gap: the fact that “logging out” can clear your own device and cut off future refresh tokens, but it can’t reach out and kill a copy of the access token that’s already out in the world. A short lifespan puts a hard ceiling on that exposure, even in the worst case.

In practice, a few strategies help narrow this gap further:

  • Keep access tokens short-lived, for exactly the reason above.
  • Actually revoke the refresh token on logout, so even though the stolen access token keeps working until it expires, the attacker can never renew it into a fresh one once that happens.

Wrapping up

Let's go back to the question that kicked off this whole article: if the server forgets you the moment one request ends, how does it know you're still you on the next one?

By now, you've seen the full answer. Instead of asking the server to remember anything, JWT flips the problem around entirely. It hands you a signed piece of proof, and lets any server verify that proof on its own, without needing a shared record, without needing to look anything up, without needing to remember a single thing about you between requests.

None of this makes JWT the "correct" answer for every app, and it isn't a replacement for sessions in every situation either. It comes with a tradeoff, just like most things in software.