Node.js API Authentication with JWT
Building secure APIs is crucial for any web application. In this guide, you’ll learn how to implement JSON Web Token (JWT) authentication in a Node.js Express API. This approach is widely used and works well with modern front-end frameworks.
If you’re new to building APIs with Node.js, you might want to check out our building REST APIs with Node.js guide first.
What is JWT Authentication?
JWT (JSON Web Token) is a compact way to securely transmit information between parties as a JSON object. It’s commonly used for authentication because it’s stateless - the server doesn’t need to store session information.
When a user logs in, the server creates a JWT containing user information and signs it with a secret key. The client then sends this token with each request to prove their identity.
Setting Up Your Project
First, create a new Node.js project and install the necessary dependencies:
mkdir nodejs-jwt-auth
cd nodejs-jwt-auth
npm init -y
npm install express jsonwebtoken bcryptjs cors dotenv
npm install -D nodemonUpdate your package.json to include a start script:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}Creating the Server
Create a server.js file with the basic Express setup:
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const cors = require('cors');
require('dotenv').config();
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Create a .env file for your environment variables:
JWT_SECRET=your-super-secret-jwt-key-make-it-long-and-randomUser Data Structure
For this tutorial, we’ll use a simple in-memory user store. In a real application, you’d use a database.
// In-memory user store (use a database in production)
let users = [];
// Helper function to find user by email
const findUserByEmail = (email) => {
return users.find(user => user.email === email);
};
// Helper function to find user by ID
const findUserById = (id) => {
return users.find(user => user.id === id);
};User Registration
Create an endpoint for user registration:
// Register endpoint
app.post('/api/register', async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user already exists
if (findUserByEmail(email)) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = {
id: Date.now().toString(),
name,
email,
password: hashedPassword
};
users.push(newUser);
// Create JWT token
const token = jwt.sign(
{ userId: newUser.id, email: newUser.email },
JWT_SECRET,
{ expiresIn: '24h' }
);
res.status(201).json({
message: 'User created successfully',
token,
user: { id: newUser.id, name: newUser.name, email: newUser.email }
});
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});User Login
Create the login endpoint:
// Login endpoint
app.post('/api/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = findUserByEmail(email);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Create JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
message: 'Login successful',
token,
user: { id: user.id, name: user.name, email: user.email }
});
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});Authentication Middleware
Create middleware to protect routes:
// Authentication middleware
const 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: 'Access token required' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
req.user = user;
next();
});
};Protected Routes
Create some protected routes that require authentication:
// Protected route - get user profile
app.get('/api/profile', authenticateToken, (req, res) => {
const user = findUserById(req.user.userId);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.json({
id: user.id,
name: user.name,
email: user.email
});
});
// Protected route - update user profile
app.put('/api/profile', authenticateToken, async (req, res) => {
try {
const { name, email } = req.body;
const userIndex = users.findIndex(u => u.id === req.user.userId);
if (userIndex === -1) {
return res.status(404).json({ message: 'User not found' });
}
// Update user info (excluding password)
users[userIndex] = {
...users[userIndex],
name: name || users[userIndex].name,
email: email || users[userIndex].email
};
const updatedUser = users[userIndex];
res.json({
message: 'Profile updated successfully',
user: { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email }
});
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});Testing Your API
You can test your API using tools like Postman or curl. Here are some example requests:
Register a new user:
curl -X POST http://localhost:3000/api/register \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"[email protected]","password":"password123"}'Login:
curl -X POST http://localhost:3000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"password123"}'Access protected route (replace TOKEN with your JWT):
curl -X GET http://localhost:3000/api/profile \
-H "Authorization: Bearer TOKEN"Frontend Integration
To use this API from a frontend application, you’d typically store the JWT in localStorage or sessionStorage and include it in the Authorization header of requests:
// Example frontend code
const api = {
async login(email, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token);
return data.user;
}
throw new Error(data.message);
},
async getProfile() {
const token = localStorage.getItem('token');
const response = await fetch('/api/profile', {
headers: { 'Authorization': `Bearer ${token}` }
});
return response.json();
}
};Security Best Practices
When implementing JWT authentication, keep these security tips in mind:
- Use strong secrets: Your JWT secret should be long, random, and kept confidential
- Set expiration: Always set an expiration time for tokens
- Use HTTPS: Always transmit tokens over HTTPS to prevent interception
- Don’t store sensitive data: Avoid putting sensitive information in the JWT payload
- Consider refresh tokens: For better security, implement refresh tokens for long sessions
- Validate input: Always validate and sanitize user input
Next Steps
This tutorial covers the basics of JWT authentication. For production applications, consider:
- Using a proper database (MongoDB, PostgreSQL, etc.)
- Implementing password reset functionality
- Adding email verification
- Using refresh tokens for better security
- Implementing rate limiting to prevent brute force attacks
- Adding logging and monitoring
For more advanced Node.js topics, check out our guide on Node.js HTTP servers or learn about async programming in JavaScript.
The official JWT documentation is an excellent resource for understanding tokens in depth, and the Express.js documentation provides more information about building robust APIs.