Skip to main content

Express JWT Authentication

Introduction

JSON Web Tokens (JWT) have become the industry standard for implementing authentication in modern web applications, especially those built with Express.js. JWTs provide a compact and self-contained way to securely transmit information between parties as a JSON object.

In this tutorial, we'll explore how to implement JWT-based authentication in an Express.js application. You'll learn what JWTs are, how they work, and how to use them to secure your Express routes.

What are JSON Web Tokens?

A JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

JWTs consist of three parts separated by dots (.):

  1. Header - Contains metadata about the token (like the type and the signing algorithm)
  2. Payload - Contains the claims or the data being transmitted
  3. Signature - Used to verify that the sender of the JWT is who it says it is

A JWT looks like this: xxxxx.yyyyy.zzzzz

Why Use JWTs for Authentication?

There are several benefits to using JWTs for authentication:

  1. Stateless - The server doesn't need to store session information
  2. Scalable - Perfect for distributed systems and microservices
  3. Secure - Information can be encrypted and signed
  4. Cross-domain - Works across different domains
  5. Compact - Can be sent through URLs, POST parameters, or inside HTTP headers

Setting up JWT Authentication in Express

Let's start by setting up a basic Express application with JWT authentication.

Step 1: Install Required Packages

bash
npm install express jsonwebtoken bcrypt dotenv

Step 2: Create a Basic Express Server

javascript
// server.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
require('dotenv').config();

const app = express();
app.use(express.json()); // For parsing application/json

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Step 3: Set Up Environment Variables

Create a .env file in your project root:

JWT_SECRET=your_jwt_secret_key
PORT=3000

The JWT_SECRET is a private key used to sign the tokens. Keep this secure and never expose it in your code or public repositories.

Step 4: Create User Registration and Login Routes

For simplicity, we'll use an in-memory user database:

javascript
// In-memory user database (in a real app, use a proper database)
const users = [];

// User registration route
app.post('/register', async (req, res) => {
try {
const { username, password } = req.body;

// Check if user already exists
const userExists = users.find(user => user.username === username);
if (userExists) {
return res.status(400).json({ message: 'User already exists' });
}

// Hash the password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);

// Create new user
const newUser = {
id: users.length + 1,
username,
password: hashedPassword
};

users.push(newUser);

res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});

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

// Find user
const user = users.find(user => user.username === username);
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}

// Validate password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}

// Create JWT payload
const payload = {
user: {
id: user.id,
username: user.username
}
};

// Sign the token
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '1h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});

Step 5: Create a Middleware to Protect Routes

javascript
// Authentication middleware
function authenticate(req, res, next) {
// Get token from header
const token = req.header('x-auth-token');

// Check if no token
if (!token) {
return res.status(401).json({ message: 'No token, authorization denied' });
}

// Verify token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded.user;
next();
} catch (err) {
res.status(401).json({ message: 'Token is not valid' });
}
}

Step 6: Create Protected Routes

javascript
// Protected route
app.get('/profile', authenticate, (req, res) => {
res.json({
message: 'Protected route accessed successfully',
user: req.user
});
});

Complete Example

Here's a complete example putting everything together:

javascript
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
require('dotenv').config();

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

const PORT = process.env.PORT || 3000;

// In-memory user database
const users = [];

// Middleware for authentication
function authenticate(req, res, next) {
const token = req.header('x-auth-token');

if (!token) {
return res.status(401).json({ message: 'No token, authorization denied' });
}

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded.user;
next();
} catch (err) {
res.status(401).json({ message: 'Token is not valid' });
}
}

// Routes
app.post('/register', async (req, res) => {
try {
const { username, password } = req.body;

const userExists = users.find(user => user.username === username);
if (userExists) {
return res.status(400).json({ message: 'User already exists' });
}

const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);

const newUser = {
id: users.length + 1,
username,
password: hashedPassword
};

users.push(newUser);

res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});

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

const user = users.find(user => user.username === username);
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}

const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}

const payload = {
user: {
id: user.id,
username: user.username
}
};

jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '1h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});

app.get('/profile', authenticate, (req, res) => {
res.json({
message: 'Protected route accessed successfully',
user: req.user
});
});

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Testing the Authentication Flow

You can test this authentication flow using tools like Postman or curl.

Registration:

Request:

POST /register
Content-Type: application/json

{
"username": "testuser",
"password": "password123"
}

Response:

json
{
"message": "User registered successfully"
}

Login:

Request:

POST /login
Content-Type: application/json

{
"username": "testuser",
"password": "password123"
}

Response:

json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Access Protected Route:

Request:

GET /profile
x-auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response:

json
{
"message": "Protected route accessed successfully",
"user": {
"id": 1,
"username": "testuser"
}
}

Real-World Considerations

While the above example demonstrates the basic concepts of JWT authentication in Express, there are several additional considerations for real-world applications:

1. Token Storage on the Client

Your frontend application needs to store the JWT somewhere. Common options include:

  • Local Storage (vulnerable to XSS attacks)
  • HTTP-only Cookies (more secure against XSS)
  • Memory (lost on page refresh)

2. Token Refresh Mechanism

JWTs are typically short-lived for security reasons. Implement a refresh token mechanism to issue new access tokens without requiring the user to log in again:

javascript
// Example refresh token route
app.post('/refresh-token', (req, res) => {
const { refreshToken } = req.body;

// Verify refresh token
// Issue new access token
});

3. Token Revocation

Unlike session-based authentication, JWTs cannot be invalidated once issued, since they are stateless. Some strategies to handle this:

  • Keep tokens short-lived
  • Implement a blacklist for revoked tokens
  • Use a database to validate tokens

4. Payload Size

JWTs can become large if you store too much information in them. Keep the payload minimal to reduce overhead.

Common JWT Security Best Practices

  1. Use HTTPS - Always transmit JWTs over HTTPS to prevent token theft
  2. Set proper expiration times - Short-lived tokens (15-60 minutes)
  3. Don't store sensitive data in JWTs - Tokens can be decoded easily
  4. Use strong secret keys - Longer, more random keys are better
  5. Implement proper error handling - Don't leak information in error responses

Summary

In this tutorial, we've explored how to implement JWT authentication in an Express.js application. We've covered:

  • What JSON Web Tokens are and why they're useful
  • Setting up a basic Express server with JWT authentication
  • Creating registration and login endpoints
  • Protecting routes with JWT middleware
  • Testing the authentication flow
  • Real-world considerations and best practices

JWT authentication offers a robust, stateless approach to user authentication that works well with modern web applications, especially those with separate frontend and backend services.

Additional Resources

  1. JWT.io - Helpful tool for decoding, verifying, and generating JWTs
  2. jsonwebtoken npm package documentation
  3. OWASP Authentication Cheatsheet

Exercises

  1. Add password requirements validation to the registration route
  2. Implement a token refresh mechanism
  3. Create a logout functionality that invalidates tokens
  4. Add role-based authorization to restrict certain routes to specific user roles
  5. Implement rate limiting to prevent brute force attacks on your authentication endpoints


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)