Skip to main content

Command Palette

Search for a command to run...

JWT Authentication in Node.js Explained Simply

Updated
6 min read
C
Software developer passionate about building scalable web applications with React and backend technologies. I enjoy solving problems, building projects, and sharing my learning with the community.

If you've ever built a Node.js app and thought, "How do I know who is calling my API?" — you've stumbled upon authentication.

Today, we’re going to break down JWT authentication in plain English. No cryptographic PhD required.

By the end, you’ll understand:

  • Why authentication exists

  • What a JWT actually is

  • How login works

  • How to protect your routes

Let’s go.


1. What Authentication Means

Authentication = answering the question: "Who are you?"

Think of it like showing your ID at an airport.
You prove your identity, then you get access.

In web apps:

  • You enter email + password

  • The server verifies them

  • If correct, you’re authenticated

Example: Logging into Twitter. Twitter checks your password. If correct, you’re in.

But here’s the problem — HTTP is stateless.


2. Stateless Authentication Simply

Stateless means: the server forgets you immediately after responding.

Imagine calling a friend, saying “I’m John,” hanging up, and calling again — they won’t remember you.

Old way (stateful):

  • Server stores your login info in memory or a database

  • You get a sessionId

  • Works, but hard to scale

JWT way (stateless):
The server gives you a “token” that contains your identity. You send it with every request. The server doesn’t need to remember you — the token proves who you are.


3. What is JWT?

JWT = JSON Web Token

It’s a string that looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6InVzZXIifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

That mess is actually three parts, separated by dots:

header.payload.signature

Structure of a JWT

Tells the server how the token was signed.

{
  "alg": "HS256",   // algorithm used
  "typ": "JWT"
}

Payload

Contains claims — information about the user.

{
  "userId": 123,
  "role": "user",
  "iat": 1712345678   // issued at (timestamp)
}

Example: This payload says “I am user 123 with role ‘user’.”

Signature

Proves the token was issued by your server and wasn’t tampered with.

Created by:

signature = hash(header + "." + payload, secret_key)

If someone changes the payload, the signature breaks — and your server rejects it.


4. Login Flow Using JWT

Let’s walk through a real example.

Step 1: User logs in

POST /api/login
Content-Type: application/json

{
  "email": "alice@example.com",
  "password": "myPassword123"
}

Step 2: Server verifies credentials

app.post("/api/login", (req, res) => {
  const { email, password } = req.body;

  // Check user in database
  const user = findUserByEmail(email);
  if (!user || user.password !== password) {
    return res.status(401).json({ message: "Invalid credentials" });
  }

  // Create JWT
  const token = jwt.sign(
    { userId: user.id, role: user.role },
    "my_super_secret_key",
    { expiresIn: "1h" }
  );

  res.json({ token });
});

Step 3: Server returns JWT

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6InVzZXIiLCJpYXQiOjE3MTIzNDU2Nzh9.abc123..."
}

Step 4: Client stores the token

Frontend saves it (usually in localStorage or cookie):

localStorage.setItem("authToken", token);

5. Sending Token with Requests

Now the user wants to see their profile.

Every request after login must include the token.

Example using fetch in frontend:

const token = localStorage.getItem("authToken");

fetch("/api/profile", {
  headers: {
    "Authorization": `Bearer ${token}`
  }
})
.then(res => res.json())
.then(data => console.log(data));

The common convention is:

Authorization: Bearer <your-jwt-token>

6. Protecting Routes Using Tokens

On the server, you create middleware that checks the token before allowing access.

JWT Middleware

function authenticateToken(req, res, next) {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ message: "No token provided" });
  }

  try {
    const decoded = jwt.verify(token, "my_super_secret_key");
    req.user = decoded;  // attach user info to request
    next();
  } catch (err) {
    return res.status(403).json({ message: "Invalid or expired token" });
  }
}

Using middleware to protect routes

// Public route
app.get("/api/public", (req, res) => {
  res.json({ message: "Anyone can see this" });
});

// Protected route
app.get("/api/profile", authenticateToken, (req, res) => {
  // req.user contains { userId: 123, role: "user", iat: ... }
  res.json({ 
    message: `Hello user ${req.user.userId}, here's your secret data` 
  });
});

Real example flow:

  1. User logs in → gets token

  2. User calls /api/profile with token

  3. Middleware verifies token

  4. If valid → profile data returned

  5. If invalid/expired → 403 Forbidden


Complete Example — Mini Auth System

Here’s everything together:

const express = require("express");
const jwt = require("jsonwebtoken");

const app = express();
app.use(express.json());

const SECRET_KEY = "my_secret_key";

// Fake database
const users = [
  { id: 1, email: "alice@example.com", password: "pass123", role: "admin" },
  { id: 2, email: "bob@example.com", password: "pass456", role: "user" }
];

// Login endpoint
app.post("/login", (req, res) => {
  const { email, password } = req.body;
  const user = users.find(u => u.email === email && u.password === password);
  
  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const token = jwt.sign(
    { userId: user.id, role: user.role },
    SECRET_KEY,
    { expiresIn: "1h" }
  );

  res.json({ token });
});

// Middleware
function auth(req, res, next) {
  const authHeader = req.headers.authorization;
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) return res.sendStatus(401);

  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    req.user = decoded;
    next();
  } catch {
    res.sendStatus(403);
  }
}

// Protected route
app.get("/profile", auth, (req, res) => {
  res.json({ message: `Welcome user ${req.user.userId}` });
});

app.listen(3000, () => console.log("Server running"));

Testing it:

# Login
curl -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"pass123"}'

# Returns token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Access profile
curl http://localhost:3000/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Summary (In Plain Words)

Concept Simple Explanation
Authentication Proving who you are
JWT A self-contained ID card
Header Tells how token is signed
Payload The actual user data
Signature Tamper-proof seal
Login Exchange credentials for token
Protect routes Check token before sending data

Final Thoughts

JWT authentication in Node.js is:

  • Stateless → no session storage needed

  • Scalable → any server can verify the token

  • Self-contained → user info inside the token

Just remember:

  • Never store sensitive data in the payload (it's only base64 encoded, not encrypted)

  • Always use HTTPS

  • Keep your secret key... secret

Now go build something awesome. 🚀


Found this helpful? Share it with a friend who’s struggling with authentication.

More from this blog

Why Node.js is Perfect for Building Fast Web Applications

Every technology makes a bet. Node.js's bet was this: most web applications aren't slow because they do too much computation. They're slow because they spend most of their time waiting — waiting for a database to respond, waiting for a file to load, waiting for an external API to return. If you build a runtime optimised around that specific reality, you get something genuinely fast for the work most web apps actually do.

May 9, 202611 min read1
C

Chetan Chauhan | Tech Blog | chetan71

41 posts