Advertisement
programming Learn how to build secure JWT authentication in Node.js and Express with login, access tokens, refresh tokens, and real-world best practices. A practical, step-by-step guide.

How do I implement secure JWT authentication in Node.js + Express step-by-step?

5 Min Read Verified Content

If you're building a modern backend — for a mobile app, SPA frontend, or API — chances are you’ll eventually need JWT authentication.

The common problem developers search for is:

“How do I build login with JWT securely?”

So let’s solve that — cleanly and safely.


🔍 What is JWT (in simple words)?

A JSON Web Token is just a signed string that proves who the user is.

Example token:

xxxxx.yyyyy.zzzzz

It contains:

✔ user data (e.g. user ID)
✔ expiry time
✔ a cryptographic signature

Think of it like a signed ticket.
If the signature is valid → the ticket is trusted.


🔧 Step 1 — Setup Project

mkdir jwt-auth-demo cd jwt-auth-demo npm init -y npm install express jsonwebtoken bcryptjs dotenv

Create a basic server:

const express = require("express"); const app = express(); app.use(express.json()); app.listen(5000, () => console.log("Server running on port 5000"));

🔑 Step 2 — Create Users (Mock for Now)

In real apps you use a database.
Here we fake it to keep things simple:

const users = [];

🔐 Step 3 — Register User (Hash Password)

const bcrypt = require("bcryptjs"); app.post("/register", async (req, res) => { const { email, password } = req.body; const hashed = await bcrypt.hash(password, 10); users.push({ email, password: hashed }); res.json({ message: "User registered" }); });

Now passwords are not stored in plain text (good).


🔑 Step 4 — Generate JWT on Login

const jwt = require("jsonwebtoken"); require("dotenv").config(); app.post("/login", async (req, res) => { const { email, password } = req.body; const user = users.find(u => u.email === email); if (!user) return res.status(400).json({ message: "User not found" }); const match = await bcrypt.compare(password, user.password); if (!match) return res.status(400).json({ message: "Wrong password" }); const token = jwt.sign( { email: user.email }, process.env.JWT_SECRET, { expiresIn: "15m" } ); res.json({ token }); });

Create .env:

JWT_SECRET=supersecretlongstring

🛡 Step 5 — Protect Routes with Middleware

function auth(req, res, next) { const header = req.headers["authorization"]; const token = header && header.split(" ")[1]; if (!token) return res.sendStatus(401); jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return res.sendStatus(403); req.user = user; next(); }); }

Use it:

app.get("/profile", auth, (req, res) => { res.json({ email: req.user.email }); });

Now:

✔ valid token → access granted
❌ no token → denied
❌ expired token → denied

Exactly how secure APIs should behave.


🔁 Bonus — Refresh Tokens (Avoid Frequent Login)

Access tokens should expire fast.

So we add a refresh token:

app.post("/refresh", (req, res) => { const { token } = req.body; jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return res.sendStatus(403); const newToken = jwt.sign( { email: user.email }, process.env.JWT_SECRET, { expiresIn: "15m" } ); res.json({ token: newToken }); }); });

Now users stay logged in without extending access token expiry dangerously.


🧠 Common Mistakes (And Fixes)

❌ Storing JWT in LocalStorage

👉 Vulnerable to XSS.

Use HTTP-only cookies when possible.


❌ Tokens That Never Expire

👉 Huge security risk.

Always set expiry.


❌ Using Weak Secrets like “12345”

👉 Use a long random secret.


📌 Quick Checklist for Production

✔ HTTPS only
✔ HttpOnly cookie if possible
✔ Short-lived access token
✔ Refresh token rotation
✔ Logout invalidates refresh token
✔ Use a real DB


🏁 Final Thoughts

JWT authentication seems scary at first — but when broken down step-by-step, it’s very logical:

👉 verify user → sign token → protect routes → expire safely

Now you have a clean foundation you can actually use in real projects.

Advertisement
Back to Programming