All articles
Backend Development

JWT authentication explained: how to secure REST APIs

November 3, 20265 min read1,023 words
JWT Authentication Flow Diagram Dot Simplify

JWT authentication is how a REST API figures out who’s calling without hitting a database on every request. The client holds a signed token. The server checks the signature and gets on with its day.

JWTs get oversold, I think. They solve one problem really well and create a few new ones if you’re sloppy. So here’s how they work, a working Node.js example, and the mistakes I’d check for first.

What a JWT is

JWT stands for JSON Web Token (the spec says to pronounce it “jot”, though most people just say the letters). It’s a string with 3 parts joined by dots:

header.payload.signature

The header names the signing algorithm. The payload carries claims: the user ID and a role, plus an expiry time. The signature is a hash of the first two parts, made with a secret key.

Header and payload are base64url-encoded JSON. Paste a token into jwt.io and you can read all of it, no key required.

So the payload is a postcard. User ID and role go in. Emails and phone numbers stay out.

The signature catches tampering: change 1 character in the payload and verification fails. Nothing hides the contents.

The claims you’ll meet most are sub (user ID), exp (expiry), iat (issued at), iss (who issued it) and aud (who it’s for).

How JWT authentication works

  1. The client sends an email and password to POST /login over HTTPS.
  2. The server checks them, signs a JWT and sends it back.
  3. From then on the client attaches the token to every request: Authorization: Bearer <token>.
  4. The server verifies the signature and the expiry. No session table, no database call.
  5. A valid token gets through. An invalid or expired one gets a 401.

Step 4 is the selling point. Any server holding the key can verify a token, so you can run 10 copies of your API behind a load balancer and none of them share session state. With microservices, each service checks the token itself.

JWT authentication in Node.js

Here’s a minimal setup with Express and the jsonwebtoken package:

const jwt = require('jsonwebtoken');

app.post('/login', async (req, res) => {

// verifyCredentials is your own function: look up the user, compare the password hash

const user = await verifyCredentials(req.body.email, req.body.password);

if (!user) return res.status(401).json({ error: 'Invalid credentials' });

const token = jwt.sign(

{ sub: user.id, role: user.role },

process.env.JWT_SECRET,

{ algorithm: 'HS256', expiresIn: '15m' }

);

res.json({ accessToken: token });

});

function auth(req, res, next) {

const header = req.headers.authorization || '';

const token = header.startsWith('Bearer ') ? header.slice(7) : null;

if (!token) return res.status(401).json({ error: 'Missing token' });

try {

req.user = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });

next();

} catch (err) {

res.status(401).json({ error: 'Invalid or expired token' });

}

}

app.get('/orders', auth, (req, res) => {

res.json({ message: `Orders for user ${req.user.sub}` });

});

Two lines carry most of the weight. expiresIn: '15m' keeps a stolen token from living long, and algorithms: ['HS256'] in verify pins the algorithm.

The secret sits in an environment variable. Hardcode it once and it lives in your git history forever.

Access tokens and refresh tokens

The server remembers nothing about a token, so it can’t cancel one. Someone steals a token that lasts 7 days, and they get 7 days of being you.

The fix is 2 tokens:

  1. Access token: lives 5 to 15 minutes, goes on every API call, sits in memory on the client.
  2. Refresh token: lives days or weeks, sits in an httpOnly cookie, with a hashed copy in your database.

When the access token dies, the client calls POST /refresh and the server hands back a new one. Rotate the refresh token on every use. To kill a stolen one, delete its row.

Yes, that puts a database lookup back in the picture. It happens once every 15 minutes instead of on every request.

How to secure a REST API with JWT

  1. Use HTTPS everywhere. Over plain HTTP, anyone on the same wifi can read the token.
  2. Keep access tokens short. I’d start at 15 minutes.
  3. Use a long random secret. For HS256 that means 32 bytes or more, and secret123 gets cracked in minutes.
  4. Pin the algorithm. In 2015, security researcher Tim McLean showed that several JWT libraries would accept a token whose header said alg: none, meaning no signature at all. Pinning the algorithm in verify shuts that door.
  5. Check the claims. Verify exp, iss and aud along with the signature.
  6. Put refresh tokens in httpOnly, Secure, SameSite cookies. localStorage is readable by any script on the page, so a single XSS hole hands over the token.
  7. Keep the payload small. A user ID and a role cover most apps.

JWT vs session authentication


SessionsJWT
Where state livesOn the server (memory, Redis or a database)Inside the token, on the client
ScalingNeeds a shared session storeAny server with the key can verify
Revoking accessDelete the session, instantHard until the token expires
Best fitServer-rendered sitesREST APIs and mobile apps

If your site renders pages on 1 server, I’d use sessions. They’re simpler, and you can kill one instantly.

JWT starts paying off when a mobile app and a few services share one API.

Mistakes I’d check for first

  1. Calling jwt.decode() where you meant jwt.verify(). In jsonwebtoken, decode reads the payload and checks nothing, so a made-up token sails right through.
  2. Leaving out exp. A token without an expiry works forever.
  3. Committing the secret to the repo. One leaked .env file and anyone can forge tokens.
  4. Calling logout done after deleting the token on the client. A copy still works until it expires, so revoke the refresh token on the server too.

Final Words

If you build 1 thing this week, build the refresh flow. The login route takes 10 minutes, and every tutorial covers it.

Refresh rotation is where the real decisions live: how long tokens last, what happens when one gets reused, how you revoke. I’d spend your time there.



#backend-development#dot-simplify#2026
Dot Simplify
Design · Build · Ship
Keep reading

More from Dot Simplify.

All articles →