JWT Authentication in Node.js Explained Simply

Apps need to know who's using them. But checking a database on every request is slow. JWT lets you prove who you are with a signed token the server can verify instantly.
This is about understanding JWT and building login systems without session databases.
What Authentication Means
Authentication answers: "Who are you?"
Real Example
Checking into a hotel: you show an ID once, and they don't check the guest list every time you walk in. The ID is your proof.
Apps work the same way.
Without Authentication
app.get("/profile", (req, res) => {
res.json({ message: "Anyone can access this" });
});
No protection.
With Authentication
app.get("/profile", (req, res) => {
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
res.json({ message: `Hello ${req.user.name}` });
});
Only authenticated users see it.
What JWT Is
JWT (JSON Web Token) is a token that represents a user's identity.
The Concept
- User logs in
- Server creates a token containing their info
- Server sends the token
- User sends the token with every request
- Server verifies the token (no database needed)
The token itself proves who they are.
Simple Analogy
Concert wristband: instead of the venue checking a list every time you enter, you wear a wristband proving you're authorized. The wristband is the proof.
JWT is like that wristband for API requests.
JWT vs Sessions
| Sessions | JWT |
|---|---|
| Stored on server | Stored on client |
| Database lookup | Token contains data |
| One server only | Works across servers |
| Can revoke immediately | Valid until expiration |
Structure of a JWT
A JWT has three parts: header.payload.signature
Part 1: Header
{
"alg": "HS256", // Algorithm
"typ": "JWT" // Type
}
Base64 encoded: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Part 2: Payload
{
"userId": 1,
"email": "alice@example.com",
"name": "Alice",
"iat": 1234567890, // Issued at
"exp": 1234571490 // Expires at
}
Base64 encoded: eyJ1c2VySWQiOjEsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20ifQ
Part 3: Signature
Server signs the header and payload:
HMACSHA256(
base64(header) + "." + base64(payload),
"secret-key"
)
Result: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Complete Token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjE...SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three parts separated by dots.
Why the Signature Matters
It proves the token hasn't been tampered with:
Valid token: signature matches
Tampered token: signature doesn't match → rejected
JWT Token Structure Visualization
JWT Token
|
├─ Header (base64)
| ├─ Algorithm: HS256
| └─ Type: JWT
|
├─ Payload (base64)
| ├─ userId: 1
| ├─ email: alice@example.com
| ├─ name: Alice
| ├─ iat: 1234567890
| └─ exp: 1234571490
|
└─ Signature (HMAC)
└─ Hash of header + payload with secret
The server can read the payload without a database because it's right there in the token.
Login Flow Using JWT
Step 1: User Logs In
app.post("/login", (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || user.password !== password) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Credentials valid
res.json({ message: "Login successful" });
});
Step 2: Server Creates JWT
const jwt = require("jsonwebtoken");
const SECRET = "your-secret-key";
app.post("/login", (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || user.password !== password) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Create token
const token = jwt.sign(
{ userId: user.id, email: user.email, name: user.name },
SECRET,
{ expiresIn: "24h" }
);
res.json({ token });
});
Token contains user info and expires in 24 hours.
Step 3: Client Stores Token
// After login, client stores token
localStorage.setItem("token", token);
Step 4: Client Sends Token With Requests
// Every request includes the token
const token = localStorage.getItem("token");
fetch("http://localhost:3000/profile", {
headers: {
"Authorization": `Bearer ${token}`
}
});
Step 5: Server Verifies Token
app.get("/profile", (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "No token" });
}
try {
const decoded = jwt.verify(token, SECRET);
res.json({ message: `Hello ${decoded.name}` });
} catch (error) {
res.status(401).json({ error: "Invalid token" });
}
});
No database needed. The token proves who they are.
JWT Login Authentication Flow
User sends login request
(email + password)
|
v
Server finds user in database
|
v
Server checks password
|
├─ Wrong: send error
└─ Correct: continue
|
v
Server creates JWT token
(contains user info)
|
v
Server sends token to client
|
v
Client stores token
(localStorage, sessionStorage, etc)
|
v
User makes API request
(includes token in Authorization header)
|
v
Server verifies token signature
|
├─ Invalid: reject (401)
└─ Valid: continue
|
v
Request allowed, response sent
Sending Token With Requests
In HTTP Header (Standard)
// Client side
const token = localStorage.getItem("token");
fetch("http://localhost:3000/profile", {
headers: {
"Authorization": `Bearer ${token}`
}
});
This is the standard approach.
In Cookie
Alternative (automatically sent):
// Server side
res.cookie("token", token, {
httpOnly: true,
secure: true,
maxAge: 24 * 60 * 60 * 1000
});
In Query String
Not recommended (exposes token in logs):
fetch("http://localhost:3000/profile?token=" + token);
The header approach is best.
Protecting Routes Using Tokens
Token Verification Middleware
function verifyToken(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "No token" });
}
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: "Invalid token" });
}
}
Apply to Routes
// Protected route
app.get("/profile", verifyToken, (req, res) => {
res.json({
message: `Hello ${req.user.name}`,
user: req.user
});
});
// Another protected route
app.get("/posts", verifyToken, (req, res) => {
res.json({ posts: "User's posts" });
});
// Public route (no middleware)
app.get("/public", (req, res) => {
res.json({ message: "Anyone can see this" });
});
The middleware checks the token before the handler runs.
Admin-Only Route
function requireAdmin(req, res, next) {
if (req.user.role !== "admin") {
return res.status(403).json({ error: "Admin only" });
}
next();
}
app.delete("/users/:id", verifyToken, requireAdmin, (req, res) => {
res.json({ message: "User deleted" });
});
Complete Login Example
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();
app.use(express.json());
const SECRET = "your-secret-key";
// In-memory user store
const users = [
{ id: 1, email: "alice@example.com", password: "password123", name: "Alice" },
{ id: 2, email: "bob@example.com", password: "password456", name: "Bob" }
];
// Login endpoint
app.post("/login", (req, res) => {
const { email, password } = req.body;
// Find user
const user = users.find(u => u.email === email);
// Check password
if (!user || user.password !== password) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Create token
const token = jwt.sign(
{ userId: user.id, email: user.email, name: user.name },
SECRET,
{ expiresIn: "24h" }
);
res.json({ token });
});
// Verify token middleware
function verifyToken(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "No token" });
}
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: "Invalid token" });
}
}
// Protected route
app.get("/profile", verifyToken, (req, res) => {
res.json({ message: `Hello ${req.user.name}`, user: req.user });
});
// Public route
app.get("/public", (req, res) => {
res.json({ message: "Public data" });
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
Test It
# Login
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"password123"}'
# Response: {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
# Access protected route with token
curl http://localhost:3000/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# Response: {"message":"Hello Alice","user":{"userId":1,...}}
Practice Assignment
1. Create a login endpoint:
// Receive email and password
// Find user in array
// Create JWT if credentials valid
// Return token
2. Build token verification middleware:
// Extract token from Authorization header
// Verify signature with jwt.verify()
// Store decoded user in req.user
// Call next() if valid
3. Protect a route:
// Apply verifyToken middleware to a route
// Route should access req.user
// Test with curl including token
4. Handle token expiration:
// Set expiresIn to "1h"
// Try accessing route after expiration
// See "invalid token" error
Quick Recap
Authentication proves who someone is.
JWT is a token containing user info, signed by the server.
JWT has three parts:
header.payload.signatureHeader specifies the algorithm.
Payload contains user data.
Signature proves the token hasn't been tampered with.
Login: receive credentials → verify → create token → send to client.
Clients send the token in the
Authorizationheader.Servers verify the token's signature, no database needed.
Protect routes by checking the token before allowing access.
JWT is stateless: the token proves who the user is.
JWT makes authentication simple and scalable.
If you enjoyed this article, check out my other blogs on this profile. 🔗 Connect with me: LinkedIn | GitHub | X (Twitter)




