Building Secure REST APIs: A Complete Authentication and Authorization Guide
Introduction
API security is often an afterthought for many developers, but it should be your first consideration when building any web application. A single security vulnerability can expose sensitive user data, compromise your entire system, or result in significant financial losses. In this comprehensive guide, we'll explore how to build secure REST APIs using modern authentication and authorization techniques.
Understanding Authentication vs Authorization
Before diving into implementation, it's crucial to understand the difference:
- Authentication: Verifying who the user is (login credentials)
- Authorization: Determining what the authenticated user can access (permissions)
Think of authentication as showing your ID at a building entrance, while authorization is having the right keycard to access specific floors.
Implementing JWT-Based Authentication
JSON Web Tokens (JWT) provide a stateless way to handle authentication. Here's a practical implementation using Node.js and Express:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const rateLimit = require('express-rate-limit');
// Rate limiting for login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts, please try again later'
});
// User registration with password hashing
app.post('/api/register', async (req, res) => {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password || password.length < 8) {
return res.status(400).json({ error: 'Invalid input' });
}
// Hash password with salt
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Save user to database
const user = await User.create({ email, password: hashedPassword });
// Generate JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.status(201).json({ token, user: { id: user.id, email: user.email } });
} catch (error) {
res.status(500).json({ error: 'Registration failed' });
}
});
// Secure login endpoint
app.post('/api/login', loginLimiter, async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token
const token = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token, user: { id: user.id, email: user.email, role: user.role } });
} catch (error) {
res.status(500).json({ error: 'Login failed' });
}
});Creating Authentication Middleware
Middleware functions help protect your routes efficiently:
// 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({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
// Role-based authorization middleware
const authorize = (roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Protected route examples
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
app.delete('/api/users/:id', authenticateToken, authorize(['admin']), async (req, res) => {
// Only admins can delete users
const userId = req.params.id;
await User.findByIdAndDelete(userId);
res.json({ message: 'User deleted successfully' });
});Input Validation and Sanitization
Always validate and sanitize user input to prevent injection attacks:
const { body, validationResult } = require('express-validator');
const xss = require('xss');
// Validation rules
const userValidationRules = () => {
return [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/),
body('name').trim().escape().isLength({ min: 2, max: 50 })
];
};
// Validation middleware
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Additional XSS protection
Object.keys(req.body).forEach(key => {
if (typeof req.body[key] === 'string') {
req.body[key] = xss(req.body[key]);
}
});
next();
};
app.post('/api/users', userValidationRules(), validate, async (req, res) => {
// Process validated and sanitized data
});Security Headers and CORS Configuration
Implement essential security headers to protect against common attacks:
const helmet = require('helmet');
const cors = require('cors');
// Security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// CORS configuration
const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? ['https://yourdomain.com']
: ['http://localhost:3000'],
credentials: true,
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));Environment Variables and Secrets Management
Never hardcode sensitive information. Use environment variables:
# .env file
JWT_SECRET=your_super_secret_jwt_key_here_make_it_long_and_random
DB_CONNECTION_STRING=your_database_connection_string
API_KEY=your_third_party_api_key
# In your application
require('dotenv').config();
const config = {
jwtSecret: process.env.JWT_SECRET,
dbConnection: process.env.DB_CONNECTION_STRING,
port: process.env.PORT || 3000
};
if (!config.jwtSecret) {
throw new Error('JWT_SECRET environment variable is required');
}Security Testing and Monitoring
Implement logging and monitoring for security events:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'security.log' })
]
});
// Log security events
const logSecurityEvent = (event, req, additional = {}) => {
logger.warn({
event,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString(),
...additional
});
};
// Example usage in authentication
if (!isValidPassword) {
logSecurityEvent('FAILED_LOGIN_ATTEMPT', req, { email });
return res.status(401).json({ error: 'Invalid credentials' });
}Conclusion
Building secure APIs requires a multi-layered approach combining authentication, authorization, input validation, and continuous monitoring. Start with these fundamentals, regularly update dependencies, and stay informed about emerging security threats. Remember that security is not a one-time implementation but an ongoing process that requires constant attention and improvement.
For production applications, consider additional measures like API gateways, Web Application Firewalls (WAF), and regular security audits. The investment in security upfront will save you from potential disasters down the road.
Related Posts
Building Secure Authentication with JWT and OAuth 2.0: A Complete Implementation Guide
Learn to implement bulletproof authentication using JWT tokens and OAuth 2.0 with practical security measures and code examples.
Building Secure User Authentication with JWT and Refresh Tokens in Node.js
Learn to implement bulletproof JWT authentication with refresh token rotation and security best practices in Node.js applications.
Building a Secure JWT Authentication System with Express.js and bcrypt
Learn to implement bulletproof JWT authentication in Node.js with proper password hashing, token management, and security best practices.