API Development
Learning Objectives
- By the end of this lesson, you will be able to:
- - Understand and implement CORS
- - Use rate limiting
- - Validate input
- - Prevent SQL injection
- - Implement HTTPS
- - Secure APIs
- - Follow security best practices
Lesson 30.3: API Security
Learning Objectives
By the end of this lesson, you will be able to:
- Understand and implement CORS
- Use rate limiting
- Validate input
- Prevent SQL injection
- Implement HTTPS
- Secure APIs
- Follow security best practices
Introduction to API Security
API security is crucial for protecting your APIs from attacks and unauthorized access. It involves multiple layers of protection.
Security Threats
- SQL Injection: Malicious SQL code injection
- XSS (Cross-Site Scripting): Script injection attacks
- CSRF (Cross-Site Request Forgery): Unauthorized actions
- DDoS: Denial of service attacks
- Man-in-the-Middle: Intercepting communications
- Brute Force: Password guessing attacks
CORS
What is CORS?
CORS (Cross-Origin Resource Sharing) is a mechanism that allows resources to be requested from a different domain.
CORS Setup
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// Enable CORS for all routes
app.use(cors());
// Or configure CORS
app.use(cors({
origin: 'https://example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
CORS Middleware
// Custom CORS middleware
const corsMiddleware = (req, res, next) => {
const allowedOrigins = [
'https://example.com',
'https://www.example.com'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
};
app.use(corsMiddleware);
Rate Limiting
What is Rate Limiting?
Rate limiting controls how many requests a client can make in a given time period.
Express Rate Limit
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// General rate limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, please try again later'
});
app.use(limiter);
// Specific route limiter
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false
});
app.post('/login', loginLimiter, (req, res) => {
// Login logic
});
Advanced Rate Limiting
const rateLimit = require('express-rate-limit');
// Store limiter (requires Redis or similar)
const createAccountLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3, // 3 accounts per hour
message: 'Too many accounts created, please try again later',
skipSuccessfulRequests: true
});
app.post('/register', createAccountLimiter, (req, res) => {
// Registration logic
});
// IP-based rate limiting
const ipLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
keyGenerator: (req) => {
return req.ip;
}
});
Input Validation
Why Validate Input?
- Security: Prevent injection attacks
- Data Integrity: Ensure correct data format
- User Experience: Provide clear error messages
Manual Validation
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function validatePassword(password) {
if (password.length < 8) {
return { valid: false, error: 'Password must be at least 8 characters' };
}
if (!/[A-Z]/.test(password)) {
return { valid: false, error: 'Password must contain uppercase letter' };
}
if (!/[a-z]/.test(password)) {
return { valid: false, error: 'Password must contain lowercase letter' };
}
if (!/[0-9]/.test(password)) {
return { valid: false, error: 'Password must contain a number' };
}
return { valid: true };
}
app.post('/register', (req, res) => {
const { email, password } = req.body;
if (!validateEmail(email)) {
return res.status(400).json({ error: 'Invalid email' });
}
const passwordValidation = validatePassword(password);
if (!passwordValidation.valid) {
return res.status(400).json({ error: passwordValidation.error });
}
// Continue registration
});
Using Validator Library
npm install validator
const validator = require('validator');
app.post('/register', (req, res) => {
const { email, password, name } = req.body;
if (!validator.isEmail(email)) {
return res.status(400).json({ error: 'Invalid email' });
}
if (!validator.isLength(password, { min: 8 })) {
return res.status(400).json({ error: 'Password too short' });
}
if (!validator.isAlphanumeric(name, 'en-US', { ignore: ' ' })) {
return res.status(400).json({ error: 'Invalid name' });
}
// Continue registration
});
Using Joi
npm install joi
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
age: Joi.number().integer().min(0).max(150).optional()
});
app.post('/register', (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({
error: error.details[0].message
});
}
// Use validated value
// Continue registration
});
Sanitization
const validator = require('validator');
function sanitizeInput(input) {
// Remove HTML tags
let sanitized = validator.escape(input);
// Trim whitespace
sanitized = sanitized.trim();
return sanitized;
}
app.post('/comment', (req, res) => {
const { comment } = req.body;
const sanitizedComment = sanitizeInput(comment);
// Use sanitized comment
});
SQL Injection Prevention
What is SQL Injection?
SQL injection is a code injection technique that exploits security vulnerabilities in database queries.
Vulnerable Code
// ❌ Bad: SQL injection vulnerability
const query = `SELECT * FROM users WHERE email = '${email}'`;
const [rows] = await pool.execute(query);
Prevention with Parameterized Queries
// ✅ Good: Parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
const [rows] = await pool.execute(query, [email]);
Using ORMs
// Sequelize (automatically prevents SQL injection)
const user = await User.findOne({
where: { email: email }
});
// Mongoose (automatically prevents injection)
const user = await User.findOne({ email });
Input Validation
function validateEmail(email) {
// Only allow valid email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
app.get('/user', async (req, res) => {
const { email } = req.query;
if (!validateEmail(email)) {
return res.status(400).json({ error: 'Invalid email' });
}
// Safe to use in query
const user = await User.findOne({ email });
res.json(user);
});
HTTPS
What is HTTPS?
HTTPS (HTTP Secure) encrypts data between client and server using SSL/TLS.
Setting Up HTTPS
# Generate self-signed certificate (development)
openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
https.createServer(options, app).listen(443, () => {
console.log('HTTPS server running on port 443');
});
Redirect HTTP to HTTPS
const express = require('express');
const app = express();
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
next();
} else {
res.redirect(`https://${req.headers.host}${req.url}`);
}
});
Security Headers
const helmet = require('helmet');
app.use(helmet());
// Or manually set headers
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});
Practice Exercise
Exercise: Securing APIs
Objective: Practice implementing CORS, rate limiting, input validation, SQL injection prevention, and HTTPS.
Instructions:
- Secure your API
- Implement CORS
- Add rate limiting
- Validate input
- Prevent SQL injection
- Practice:
- CORS configuration
- Rate limiting
- Input validation
- Security headers
- HTTPS setup
Example Solution:
// src/middleware/security.js
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
// Security headers
const securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
});
// Rate limiters
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: 'Too many requests, please try again later'
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many authentication attempts, please try again later',
skipSuccessfulRequests: true
});
const createAccountLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3,
message: 'Too many accounts created, please try again later'
});
module.exports = {
securityHeaders,
generalLimiter,
authLimiter,
createAccountLimiter
};
// src/middleware/validation.js
const Joi = require('joi');
const validate = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
success: false,
errors
});
}
req.body = value;
next();
};
};
// Validation schemas
const registerSchema = Joi.object({
name: Joi.string().min(3).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])')).required()
});
const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
});
module.exports = {
validate,
registerSchema,
loginSchema
};
// src/middleware/cors.js
const cors = require('cors');
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = [
'http://localhost:3000',
'https://example.com'
];
// Allow requests with no origin (mobile apps, Postman, etc.)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // 24 hours
};
module.exports = cors(corsOptions);
// src/routes/auth.js
const express = require('express');
const router = express.Router();
const { validate, registerSchema, loginSchema } = require('../middleware/validation');
const { authLimiter, createAccountLimiter } = require('../middleware/security');
// Register with validation and rate limiting
router.post('/register',
createAccountLimiter,
validate(registerSchema),
async (req, res) => {
// Registration logic
}
);
// Login with validation and rate limiting
router.post('/login',
authLimiter,
validate(loginSchema),
async (req, res) => {
// Login logic
}
);
module.exports = router;
// src/app.js
const express = require('express');
const mongoose = require('mongoose');
const { securityHeaders, generalLimiter } = require('./middleware/security');
const corsOptions = require('./middleware/cors');
const authRouter = require('./routes/auth');
require('dotenv').config();
const app = express();
// Security middleware
app.use(securityHeaders);
app.use(corsOptions);
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Rate limiting
app.use('/api/', generalLimiter);
// Database connection
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb')
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
// Routes
app.use('/api/auth', authRouter);
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// package.json
{
"name": "api-security-practice",
"version": "1.0.0",
"description": "API security practice",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js"
},
"dependencies": {
"express": "^4.18.0",
"mongoose": "^7.0.0",
"cors": "^2.8.5",
"helmet": "^7.0.0",
"express-rate-limit": "^6.7.0",
"joi": "^17.9.0",
"dotenv": "^16.0.0"
},
"devDependencies": {
"nodemon": "^2.0.0"
}
}
Expected Output:
- CORS configured
- Rate limiting active
- Input validation working
- Security headers set
- API secured
Challenge (Optional):
- Add request logging
- Implement API key authentication
- Add IP whitelisting
- Create security monitoring
Common Mistakes
1. Not Using Parameterized Queries
// ❌ Bad: SQL injection vulnerability
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ✅ Good: Parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
const [rows] = await pool.execute(query, [email]);
2. Weak CORS Configuration
// ❌ Bad: Allow all origins
app.use(cors());
// ✅ Good: Specific origins
app.use(cors({
origin: ['https://example.com']
}));
3. No Input Validation
// ❌ Bad: No validation
app.post('/user', (req, res) => {
const user = await User.create(req.body);
});
// ✅ Good: Validate input
app.post('/user', validate(userSchema), (req, res) => {
const user = await User.create(req.body);
});
Key Takeaways
- CORS: Control cross-origin requests
- Rate Limiting: Prevent abuse
- Input Validation: Ensure data integrity
- SQL Injection: Use parameterized queries
- HTTPS: Encrypt communications
- Security Headers: Add extra protection
- Best Practice: Multiple layers of security
Quiz: API Security
Test your understanding with these questions:
-
CORS:
- A) Cross-Origin Resource Sharing
- B) Cross-Origin Request Sharing
- C) Both
- D) Neither
-
Rate limiting:
- A) Prevents abuse
- B) Allows abuse
- C) Both
- D) Neither
-
SQL injection:
- A) Prevented with parameterized queries
- B) Not prevented
- C) Both
- D) Neither
-
HTTPS:
- A) Encrypts data
- B) Doesn't encrypt data
- C) Both
- D) Neither
-
Input validation:
- A) Important for security
- B) Not important
- C) Both
- D) Neither
-
Security headers:
- A) Add protection
- B) Don't add protection
- C) Both
- D) Neither
-
Parameterized queries:
- A) Prevent SQL injection
- B) Don't prevent SQL injection
- C) Both
- D) Neither
Answers:
- A) Cross-Origin Resource Sharing
- A) Prevents abuse
- A) Prevented with parameterized queries
- A) Encrypts data
- A) Important for security
- A) Add protection
- A) Prevent SQL injection
Next Steps
Congratulations! You've completed Module 30: Databases and Authentication. You now know:
- How to work with databases
- How to implement authentication
- How to secure APIs
- How to protect applications
What's Next?
- Course 9: Practical Projects
- Build real-world applications
- Apply all learned concepts
- Create portfolio projects
Additional Resources
- OWASP: owasp.org
- CORS: developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- Helmet: helmetjs.github.io
- Rate Limiting: github.com/express-rate-limit/express-rate-limit
Lesson completed! You've finished Module 30: Databases and Authentication. Ready for Course 9: Practical Projects!