Express API Authentication
In this guide, we'll learn how to implement authentication in Express.js REST APIs. Authentication is a critical component of web applications that ensures only authorized users can access certain resources or perform specific actions.
Introduction to API Authentication
Authentication is the process of verifying the identity of users or systems attempting to access your application. For RESTful APIs, authentication is particularly important since APIs often serve as the gateway to sensitive data and operations.
As a stateless protocol, HTTP doesn't maintain session information between requests. This presents unique challenges for authentication in REST APIs, leading to various authentication strategies.
Why Authentication Matters
- Security: Prevents unauthorized access to sensitive data
- User-specific content: Allows personalized experiences
- Resource protection: Controls who can perform certain operations
- Accountability: Tracks actions performed by specific users
Common Authentication Strategies for Express APIs
1. Basic Authentication
The simplest form of authentication, where credentials are sent with each request.
// Basic Auth Middleware Example
const basicAuth = (req, res, next) => {
// Get authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
return res.status(401).json({ message: 'Authentication required' });
}
// Extract and decode credentials
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf8');
const [username, password] = credentials.split(':');
// Validate credentials (in a real app, check against database)
if (username === 'admin' && password === 'password') {
req.user = { username };
next();
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
};
app.get('/api/protected', basicAuth, (req, res) => {
res.json({ message: `Welcome, ${req.user.username}!` });
});
Pros: Simple to implement Cons: Credentials sent with every request, lacks many security features
2. Session-based Authentication
Uses cookies to maintain session state between requests.
const express = require('express');
const session = require('express-session');
const app = express();
// Set up session middleware
app.use(express.json());
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === 'production', maxAge: 3600000 } // 1 hour
}));
// Login route
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// In a real app, validate against database
if (username === 'user' && password === 'password') {
// Store user info in session
req.session.user = { username, role: 'user' };
res.json({ message: 'Login successful' });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
// Protected route
app.get('/api/profile', (req, res) => {
if (!req.session.user) {
return res.status(401).json({ message: 'Not authenticated' });
}
res.json({ user: req.session.user, message: 'Protected data' });
});
// Logout route
app.post('/api/logout', (req, res) => {
req.session.destroy();
res.json({ message: 'Logged out successfully' });
});
Pros: User-friendly, stateful Cons: Requires session storage, doesn't scale as easily in distributed systems
3. JWT (JSON Web Token) Authentication
The most popular method for REST APIs, using signed tokens to verify identity.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const JWT_SECRET = 'your-secret-key'; // Store in environment variables in production
// Login and generate token
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// In a real app, validate against database
if (username === 'user' && password === 'password') {
// Create payload
const payload = {
user: {
id: 123,
username,
role: 'user'
}
};
// Sign the JWT
jwt.sign(
payload,
JWT_SECRET,
{ expiresIn: '1h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
// Middleware to verify token
const authenticateToken = (req, res, next) => {
// Get token from header
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
// Verify token
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Invalid token' });
}
// Add user data to request
req.user = decoded.user;
next();
});
};
// Protected route
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({
message: 'Protected data',
user: req.user
});
});
Pros: Stateless, scalable, can include user info in token Cons: Can't invalidate tokens before expiration without additional mechanisms
Implementing Role-based Access Control
Often, you need different permission levels for different users. Here's how to implement role-based access control:
// Middleware to check user role
const checkRole = (role) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: 'Not authenticated' });
}
if (req.user.role !== role) {
return res.status(403).json({ message: 'Not authorized' });
}
next();
};
};
// Routes with role-based protection
app.get('/api/admin/dashboard', authenticateToken, checkRole('admin'), (req, res) => {
res.json({ message: 'Admin dashboard data' });
});
app.get('/api/user/profile', authenticateToken, checkRole('user'), (req, res) => {
res.json({ message: 'User profile data' });
});
Best Practices for API Authentication
- Use HTTPS: Always use HTTPS to encrypt data in transit
- Store Secrets Securely: Keep JWT secrets, session secrets, and API keys in environment variables
- Hash Passwords: Never store plain text passwords; use bcrypt or Argon2
- Rate Limiting: Implement rate limiting to prevent brute force attacks
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 login attempts per window
message: 'Too many login attempts, please try again after 15 minutes'
});
app.post('/api/login', loginLimiter, (req, res) => {
// Login logic
});
- Token Expiration: Set reasonable expiration times for tokens
- Refresh Tokens: Implement refresh token strategy for better security
Real-World Authentication Example
Let's put everything together into a more complete example using JWT with refresh tokens:
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { v4: uuidv4 } = require('uuid');
const app = express();
app.use(express.json());
// In a real app, store these in a database
const users = [
{
id: '1',
username: 'user1',
password: '$2b$10$eCKJeYdpgxkjVKaby./IGOxDJKjViXkKP3KXIyhjmS5wjxGuo/JCm', // "password123"
role: 'user'
},
{
id: '2',
username: 'admin',
password: '$2b$10$eCKJeYdpgxkjVKaby./IGOxDJKjViXkKP3KXIyhjmS5wjxGuo/JCm', // "password123"
role: 'admin'
}
];
// In a real app, store these in a database (Redis recommended)
let refreshTokens = [];
const ACCESS_TOKEN_SECRET = 'access-token-secret';
const REFRESH_TOKEN_SECRET = 'refresh-token-secret';
// Login endpoint
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// Find user
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check password
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Create tokens
const accessToken = generateAccessToken(user);
const refreshToken = jwt.sign({ userId: user.id }, REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
// Store refresh token
refreshTokens.push(refreshToken);
res.json({ accessToken, refreshToken });
});
// Generate access token
function generateAccessToken(user) {
return jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
}
// Refresh token endpoint
app.post('/api/refresh-token', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh token required' });
}
if (!refreshTokens.includes(refreshToken)) {
return res.status(403).json({ message: 'Invalid refresh token' });
}
jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Invalid refresh token' });
}
const user = users.find(u => u.id === decoded.userId);
if (!user) {
return res.status(403).json({ message: 'User not found' });
}
const accessToken = generateAccessToken(user);
res.json({ accessToken });
});
});
// Logout endpoint
app.post('/api/logout', (req, res) => {
const { refreshToken } = req.body;
refreshTokens = refreshTokens.filter(token => token !== refreshToken);
res.json({ message: 'Logged out successfully' });
});
// Authentication middleware
const authenticateToken = (req, res, next) => {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, ACCESS_TOKEN_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Invalid token' });
}
req.user = decoded;
next();
});
};
// Protected route
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
message: 'Protected data',
user: req.user
});
});
// Admin route
app.get('/api/admin', authenticateToken, (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ message: 'Access denied' });
}
res.json({ message: 'Admin data' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Authentication with Third-Party Services
Many applications use third-party authentication providers like OAuth 2.0 services (Google, Facebook, GitHub, etc.).
Here's a basic example using Passport.js with Google OAuth:
const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const session = require('express-session');
const app = express();
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
passport.use(new GoogleStrategy({
clientID: 'GOOGLE_CLIENT_ID',
clientSecret: 'GOOGLE_CLIENT_SECRET',
callbackURL: 'http://localhost:3000/auth/google/callback'
},
function(accessToken, refreshToken, profile, done) {
// In a real app, find or create user in database
return done(null, profile);
}
));
// Serialize and deserialize user
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
// Routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication
res.redirect('/profile');
}
);
// Check if user is authenticated
function isLoggedIn(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
app.get('/profile', isLoggedIn, (req, res) => {
res.send(`<h1>Hello ${req.user.displayName}</h1><img src="${req.user.photos[0].value}">`);
});
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
app.listen(3000);
Summary
In this guide, we've covered several approaches to implementing authentication in Express.js REST APIs:
- Basic Authentication: Simple but less secure
- Session-based Authentication: Good for browser-based applications
- JWT Authentication: Excellent for stateless APIs
- Role-based Access Control: For managing different permission levels
- Third-party Authentication: Leveraging external identity providers
The best authentication method depends on your specific requirements. JWTs are generally the preferred choice for modern Express REST APIs due to their stateless nature and scalability.
Remember that authentication is just one part of a comprehensive security strategy. Always follow security best practices like using HTTPS, properly hashing passwords, and implementing rate limiting.
Additional Resources
- JSON Web Tokens (JWT) Specification
- Express.js Security Best Practices
- Passport.js Documentation
- OWASP Authentication Cheat Sheet
Exercises
- Implement JWT authentication with refresh tokens in an Express API
- Add role-based access control to protect different routes
- Implement OAuth 2.0 authentication using Passport.js with a provider of your choice
- Create a password reset functionality with token-based verification
- Implement multi-factor authentication using a library like Speakeasy for time-based one-time passwords (TOTP)
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)