Skip to main content

Command Palette

Search for a command to run...

JWT Authentication in Node.js Explained Simply

Published
7 min read
JWT Authentication in Node.js Explained Simply

Your application has user accounts. Some routes should only be accessible to logged-in users. How do you protect routes and verify that a request actually comes from who they claim to be?

JWT (JSON Web Token) is a popular, stateless way to handle authentication. Once a user logs in, you give them a token. They include this token with every request, and you verify it to confirm they're authenticated.

Why Authentication Exists

Imagine a banking app. You log in, and your account is yours alone. If anyone could access anyone else's account, the app would be useless and insecure.

Authentication answers one question: "Who are you?" Once you've answered that question by logging in, the app trusts that you are who you claim to be.

What Is JWT?

JWT is a self-contained token that includes:

  1. User information (claims)

  2. A signature to verify it hasn't been tampered with

  3. An expiry time so old tokens eventually become invalid

It's like a digital ID card that proves you are who you say you are.

JWT Structure

A JWT has three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Part 1: Header

{
  "alg": "HS256",
  "typ": "JWT"
}

This tells you the algorithm used to sign the token and that it's a JWT.

Part 2: Payload (Claims)

{
  "userId": 123,
  "username": "john",
  "email": "john@example.com",
  "iat": 1516239022,
  "exp": 1516325422
}

This is the data about the user. iat is when it was issued, exp is when it expires.

Part 3: Signature

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

This is created by signing the header and payload with a secret key. It prevents tampering.

Login Flow with JWT

Here's how JWT authentication works:

  1. User logs in → Sends username and password

  2. Server validates → Checks credentials in database

  3. Server creates token → Encodes user info into JWT

  4. Server returns token → Sends JWT to client

  5. Client stores token → Saves it in localStorage or a cookie

  6. Client sends token → Includes it with every request

  7. Server verifies token → Checks signature and expiry

  8. Server grants access → If valid, allows the request

Setting Up JWT in Express

First, install the jsonwebtoken package:

npm install jsonwebtoken

Here's a basic example:

import express from 'express';
import jwt from 'jsonwebtoken';

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

const SECRET_KEY = 'your-secret-key-keep-it-safe';

// Login route
app.post('/login', (req, res) => {
  // In real apps, validate username/password against database
  const user = {
    userId: 123,
    username: 'john',
    email: 'john@example.com'
  };

  // Create JWT token
  const token = jwt.sign(user, SECRET_KEY, { expiresIn: '1h' });

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

app.listen(3000, () => {
  console.log('Server running');
});

When someone posts to /login, they receive a JWT token.

Verifying Tokens

Now protect routes by checking if the token is valid:

import express from 'express';
import jwt from 'jsonwebtoken';

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

const SECRET_KEY = 'your-secret-key-keep-it-safe';

// Middleware to verify token
function verifyToken(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];

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

  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

app.post('/login', (req, res) => {
  const user = { userId: 123, username: 'john' };
  const token = jwt.sign(user, SECRET_KEY, { expiresIn: '1h' });
  res.json({ token });
});

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

app.listen(3000);

The verifyToken middleware checks the token before allowing access to /profile.

How to Send a Token

After login, clients include the token in the Authorization header:

// Client-side (in browser or mobile app)
const token = 'eyJhbGc...'; // Token received from login

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

The format is: Authorization: Bearer [token]

The server extracts the token after the word "Bearer".

Complete Authentication Example

Here's a complete login and protected route system:

import express from 'express';
import jwt from 'jsonwebtoken';

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

const SECRET_KEY = 'super-secret-key';

// Sample user database
const users = [
  { id: 1, username: 'john', password: 'password123' },
  { id: 2, username: 'jane', password: 'password456' }
];

// Middleware to verify token
function verifyToken(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];

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

  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}

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

  // Find user (in real apps, check against hashed passwords)
  const user = users.find(u => u.username === username && u.password === password);

  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Create token
  const token = jwt.sign(
    { userId: user.id, username: user.username },
    SECRET_KEY,
    { expiresIn: '24h' }
  );

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

// Protected route
app.get('/profile', verifyToken, (req, res) => {
  res.json({
    message: `Hello ${req.user.username}`,
    userId: req.user.userId
  });
});

// Another protected route
app.get('/dashboard', verifyToken, (req, res) => {
  res.json({ message: 'Dashboard for ' + req.user.username });
});

app.listen(3000, () => {
  console.log('Auth server running');
});

Token Expiry

Tokens expire to limit damage if a token is stolen. Create tokens with an expiry:

const token = jwt.sign(user, SECRET_KEY, { expiresIn: '1h' });

After 1 hour, the token is invalid, and the user must log in again.

When someone uses an expired token, jwt.verify throws an error, and they get a 401 response.

Refresh Tokens

For better user experience, use refresh tokens. These are long-lived tokens that generate new short-lived access tokens:

app.post('/login', (req, res) => {
  const user = { userId: 123, username: 'john' };

  // Short-lived access token
  const accessToken = jwt.sign(user, SECRET_KEY, { expiresIn: '15m' });

  // Long-lived refresh token
  const refreshToken = jwt.sign(user, REFRESH_SECRET, { expiresIn: '7d' });

  res.json({ accessToken, refreshToken });
});

app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
    const newAccessToken = jwt.sign(
      { userId: decoded.userId, username: decoded.username },
      SECRET_KEY,
      { expiresIn: '15m' }
    );
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Users use the short-lived access token for requests. When it expires, they use the refresh token to get a new access token without logging in again.

Important Security Practices

Keep your secret safe: Never commit it to Git. Use environment variables:

const SECRET_KEY = process.env.JWT_SECRET;

Use HTTPS only: Always transmit tokens over HTTPS to prevent interception.

Don't store sensitive data in tokens: JWT tokens are encoded, not encrypted. Anyone can decode them and read the payload. Don't include passwords or payment info.

Set reasonable expiry times: Shorter expiry means less damage if a token is stolen. Balance with user convenience.

Hash passwords: Never store plain passwords in your database. Always hash them.

Common Mistakes

Mistake 1: Storing passwords in JWT

// Wrong
const token = jwt.sign({
  username: user.username,
  password: user.password
}, SECRET_KEY);

// Right
const token = jwt.sign({
  userId: user.id,
  username: user.username
}, SECRET_KEY);

Mistake 2: Using a weak secret

// Wrong - too simple
const SECRET_KEY = 'secret';

// Better - long random string
const SECRET_KEY = process.env.JWT_SECRET;

Mistake 3: Not checking token expiry

jwt.verify handles expiry checking automatically. Don't skip it.

Key Takeaways

  • JWT is a self-contained token with user information and a signature

  • Users get a token after login and include it with every request

  • Tokens expire to limit damage if stolen

  • Verify tokens with jwt.verify before allowing access

  • Use middleware to check tokens on protected routes

  • Keep your secret key safe and use environment variables

  • Tokens are encoded, not encrypted, so don't store sensitive data

  • Use refresh tokens for better user experience with short-lived access tokens

JWT is a powerful, flexible way to handle authentication in Node.js applications. Once you understand the basics, you can build secure APIs.