Node.js Authentication with JWT

Authentication is the process of verifying who a user is. JWT (JSON Web Token) is a compact, self-contained token format used to securely transmit information between parties. It is one of the most widely used approaches for authentication in REST APIs and Node.js applications.

When a user logs in successfully, the server creates a JWT and sends it to the client. The client stores it and sends it back with every subsequent request. The server verifies the token to confirm the user's identity — without needing to store any session data on the server.

How JWT Authentication Works

  1. The user submits their credentials (username and password) to the login endpoint.
  2. The server verifies the credentials. If correct, it creates and signs a JWT using a secret key.
  3. The JWT is returned to the client (browser, mobile app, etc.).
  4. The client stores the token (typically in memory or local storage).
  5. For every protected request, the client sends the token in the HTTP header: Authorization: Bearer <token>.
  6. The server verifies the token's signature using the same secret key. If valid, the request proceeds; if not, it is rejected.

JWT Structure

A JWT consists of three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDg2NDAwfQ.Xm9qXb4vQ_abc123signature
  • Header: Algorithm and token type (base64-encoded).
  • Payload: The claims — data about the user like ID and role (base64-encoded, NOT encrypted).
  • Signature: Used to verify the token was not tampered with.

Important: The payload is only encoded, not encrypted. Do not store sensitive data like passwords in a JWT payload.

Installing Required Packages

npm install express jsonwebtoken bcryptjs dotenv
  • jsonwebtoken — creates and verifies JWTs.
  • bcryptjs — hashes passwords securely before storing them.
  • dotenv — loads environment variables from .env.

Setting Up the .env File

PORT=3000
JWT_SECRET=myVerySecretKey_ChangeThis_InProduction
JWT_EXPIRES_IN=1d

Building the Authentication System

// app.js
require('dotenv').config();

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

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

// Simulated user database (use MongoDB/Mongoose in a real app)
const users = [];

// ─────────────────────────────
// REGISTER – Create a new user
// ─────────────────────────────
app.post('/api/register', async function(req, res) {
  const { name, email, password } = req.body;

  if (!name || !email || !password) {
    return res.status(400).json({ error: 'All fields are required.' });
  }

  // Check if email already exists
  const existingUser = users.find(u => u.email === email);
  if (existingUser) {
    return res.status(400).json({ error: 'Email already registered.' });
  }

  // Hash the password before saving (never store plain text passwords)
  const hashedPassword = await bcrypt.hash(password, 10);

  const newUser = {
    id: users.length + 1,
    name,
    email,
    password: hashedPassword,
    role: 'user'
  };

  users.push(newUser);

  res.status(201).json({ message: 'User registered successfully', userId: newUser.id });
});

// ─────────────────────────────
// LOGIN – Authenticate a user
// ─────────────────────────────
app.post('/api/login', async function(req, res) {
  const { email, password } = req.body;

  // Find the user
  const user = users.find(u => u.email === email);
  if (!user) {
    return res.status(401).json({ error: 'Invalid email or password.' });
  }

  // Compare the provided password with the hashed password
  const passwordMatch = await bcrypt.compare(password, user.password);
  if (!passwordMatch) {
    return res.status(401).json({ error: 'Invalid email or password.' });
  }

  // Create and sign the JWT
  const token = jwt.sign(
    { userId: user.id, email: user.email, role: user.role }, // Payload
    process.env.JWT_SECRET,                                  // Secret key
    { expiresIn: process.env.JWT_EXPIRES_IN }                // Options
  );

  res.json({
    message: 'Login successful',
    token,
    user: { id: user.id, name: user.name, email: user.email, role: user.role }
  });
});

app.listen(process.env.PORT, () => {
  console.log(`Auth server running on port ${process.env.PORT}`);
});

Creating the JWT Middleware

This middleware verifies the token for every protected route:

// middleware/auth.js

const jwt = require('jsonwebtoken');

function protect(req, res, next) {
  const authHeader = req.headers['authorization'];

  // Token must be in the format: "Bearer "
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Access denied. No token provided.' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Attach user info to the request
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token.' });
  }
}

module.exports = protect;

Protecting Routes with the Middleware

const protect = require('./middleware/auth');

// Public route — no token required
app.get('/api/public', function(req, res) {
  res.json({ message: 'This is a public endpoint.' });
});

// Protected route — token required
app.get('/api/profile', protect, function(req, res) {
  res.json({
    message: 'Profile data for authenticated user.',
    user: req.user
  });
});

// Protected route — only for admins
app.delete('/api/admin/users/:id', protect, function(req, res) {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden. Admin access only.' });
  }
  res.json({ message: 'User ' + req.params.id + ' deleted by admin.' });
});

Testing the Authentication Flow

Step 1 – Register

curl -X POST http://localhost:3000/api/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com","password":"mypassword"}'

Step 2 – Login and Receive Token

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

Step 3 – Access Protected Route with Token

curl http://localhost:3000/api/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Token Expiry and Refresh Tokens

JWTs are set to expire after a period (e.g., 1d = one day). When a token expires, the client receives a 401 error. A common pattern is to use a short-lived access token (15–60 minutes) and a long-lived refresh token (7–30 days) to request a new access token without requiring the user to log in again. This pattern is more advanced and is typically implemented in production applications.

Key Points

  • JWT is a stateless authentication mechanism — the server stores no session data.
  • A JWT contains a signed payload with user data (like ID and role) that the server can verify.
  • Always hash passwords with bcryptjs before storing them. Never store plain text passwords.
  • Use jwt.sign(payload, secret, options) to create a token and jwt.verify(token, secret) to validate it.
  • Send tokens in the Authorization: Bearer <token> header for every protected request.
  • The JWT secret key must be stored in an environment variable — never hardcoded in the source code.
  • Middleware is the clean way to protect routes — define it once and apply it where needed.
  • The payload is base64-encoded, not encrypted — do not store passwords or other secrets in the token.

Leave a Comment

Your email address will not be published. Required fields are marked *