Skip to main content

Command Palette

Search for a command to run...

JWT Authentication in Node.js Explained Simply

How JWT Authentication Works in Real Applications

Updated
8 min read
JWT Authentication in Node.js Explained Simply
A

Hi, I’m Abdul Samad. A web development learner and tech enthusiast. I write about what I learn, share practical coding tips, and publish in-depth blogs on programming and modern web development.

Check out my full collection of blogs on Hashnode: https://abdulsamad30.hashnode.dev/

Connect with me on X for quick updates and insights: @abdul_sama60108

Building a web application is fun until you have to figure out how to keep user data safe. You can't just let anyone access private profiles or delete someone else's posts. That's where authentication comes in.

If you've heard terms like "JWT," "tokens," or "stateless auth" and felt a bit lost, don't worry. We're going to break it down into plain English, skip the heavy math, and focus on how it actually works in a real Node.js app.


What Authentication Means

At its core, authentication is simply the process of verifying who you are. It's the digital equivalent of showing your ID at a club entrance or using a key to unlock your front door. Without it, the server has no idea if you're the account owner or a stranger trying to snoop around.

In the old days, servers used sessions (like a guest list at the door) to remember logged-in users. But as apps grew bigger and started serving mobile phones, tablets, and browsers simultaneously, managing that guest list became messy. This led to a smarter approach: Token-based authentication.

Think of it like a wristband at a music festival. Once security checks your ticket (your login credentials) at the gate, they give you a wristband (the token). Now, instead of showing your ID every time you want water or entry to a VIP area, you just flash your wristband. The staff trusts the wristband, not your face.


What is JWT?

JWT stands for JSON Web Token. It's a compact, self-contained way to securely transmit information between parties as a JSON object.

The magic of JWT is that it's stateless. The server doesn't need to store a session in a database or memory to remember you. All the necessary information about who you are is encoded inside the token itself. When you send the token back, the server can verify it instantly without looking anything up.

It's like having a sealed, tamper-proof letter. If the seal is broken, the server knows someone tried to cheat. If the seal is intact, the server trusts the contents immediately.


Structure of a JWT

A JWT isn't just a random string of characters; it has a specific structure made of three parts separated by dots: Header.Payload.Signature.

1. Header

The header typically consists of two parts: the type of the token (which is JWT) and the signing algorithm being used (like HMAC SHA256 or RSA). It's essentially the metadata describing how the token was created.

Analogy: Think of this as the envelope type and the lock mechanism specified on the outside of a secure package.

const header = {
  alg: 'HS256',
  typ: 'JWT'
};
// Expected Output when base64Url encoded:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Payload

The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are registered claims (like expiration time), public claims, and private claims. Usually, you put the user ID, username, or role here.

Important: The payload is not encrypted, only encoded. Anyone can decode it and read it, so never put passwords or sensitive secrets here.

Analogy: This is the actual letter inside the envelope stating, "This person is John Doe, User ID 123, and is allowed in the VIP section."

const payload = {
  sub: '1234567890',
  name: 'John Doe',
  admin: true
};
// Expected Output when base64Url encoded:
// eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

3. Signature

To create the signature part, you have to take the encoded header, the encoded payload, a secret key, and the algorithm specified in the header, and sign them. This ensures that the token hasn't been altered along the way. If even one character in the header or payload changes, the signature becomes invalid.

Analogy: This is the wax seal stamped over the envelope flap. If someone tries to open the letter and change the name inside, the wax seal breaks, and the receiver knows it's fake.

// Pseudo-code representation of signing
const signature = HMACSHA256(
  base64UrlEncode(header) + '.' + base64UrlEncode(payload),
  'your-256-bit-secret'
);
// Expected Output: A long string of random-looking characters
// SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

When combined, the final token looks like this: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c


The Login Flow Using JWT

Now that we know what a token is, let's see how it fits into the login process. This is where the magic happens in your Node.js backend.

  1. The user sends their username and password to the server.

  2. The server verifies the credentials against the database.

  3. If correct, the server generates a JWT using a secret key and sends it back to the client.

  4. The client stores this token (usually in LocalStorage or a cookie).

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

app.use(express.json());

const SECRET_KEY = 'super_secret_key_123'; // In production, use .env variables!

app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // Mock database check
  if (username === 'admin' && password === 'password123') {
    const token = jwt.sign({ username, role: 'admin' }, SECRET_KEY, { expiresIn: '1h' });

    return res.json({
      message: 'Login successful',
      token: token
    });
  }

  return res.status(401).json({ message: 'Invalid credentials' });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Expected Output (upon successful login):

{
  "message": "Login successful",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjk5OTk5OTk5LCJleHAiOjE2OTk5OTk5OTl9.abc123signature..."
}

This diagram visualizes the journey: Client sends credentials → Server validates → Server issues Token → Client receives Token. Notice the server doesn't save a session; it just hands over the key (the token).


Sending Token with Requests

Once the client has the token, they need to prove they are logged in for every subsequent request (like fetching a profile or posting a comment). The standard practice is to send the token in the Authorization Header of the HTTP request.

The format usually looks like this: Authorization: Bearer <your_token_here>

"Bearer" is just a keyword telling the server, "I'm holding this token, please let me in."

Analogy: Imagine walking up to a secure door. Instead of typing your password again, you hold up your wristband (the token) to the scanner. The scanner reads the band and lets you pass.

Here is how you might send it from a frontend using fetch:

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // The token received from login

fetch('<http://localhost:3000/profile>', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  }
})
.then(response => response.json())
.then(data => console.log(data));

Expected Output (Console Log):

{ userId: '123', username: 'admin', data: 'Private user info...' }

Protecting Routes Using Tokens

Now for the most critical part: making sure unauthorized users can't access protected routes. We do this by creating middleware. Middleware is code that runs before your main route handler.

The middleware will:

  1. Check if the Authorization header exists.

  2. Extract the token.

  3. Verify the token using the same secret key used during login.

  4. If valid, attach the user data to the request and let it proceed.

  5. If invalid, block the request and send an error.

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

  if (!token) {
    return res.sendStatus(401); // No token provided
  }

  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      return res.sendStatus(403); // Invalid or expired token
    }
    req.user = user; // Attach user data to request
    next(); // Proceed to the next middleware/route
  });
};

// Protected Route
app.get('/profile', authenticateToken, (req, res) => {
  // Only reachable if token is valid
  res.json({
    message: 'Welcome to your private profile!',
    user: req.user
  });
});

Scenario A: Request with valid tokenOutput:

{
  "message": "Welcome to your private profile!",
  "user": { "username": "admin", "role": "admin", "iat": 1699999999, "exp": 1700003599 }
}

Scenario B: Request with no token or bad tokenOutput:

Forbidden (or Unauthorized) - Status Code 403/401

This diagram illustrates the lifecycle of a request hitting a protected route: Request arrives → Middleware intercepts → Token extracted → Signature verified → Access granted or denied.


Wrapping Up

JWT authentication might sound complex because of the cryptography involved under the hood, but the concept is surprisingly simple. It shifts the burden of remembering users from the server's memory to a secure token held by the client.

We covered:

  • Why we need authentication (to verify identity).

  • What JWT is (a stateless, self-contained token).

  • The three parts of a token: Header, Payload, and Signature.

  • How the login flow generates the token.

  • How clients send the token back via headers.

  • How to protect routes using middleware to verify that token.

By implementing this pattern, you build APIs that are scalable, secure, and ready for modern applications whether they are accessed via a browser, a mobile app, or a third-party service. Just remember to keep your SECRET_KEY safe and never store sensitive data in the payload!

FIN ✌️