Secure Software Development Cheat Sheet
A comprehensive guide to implementing secure authentication, complete with examples and advice
Key Security Concepts
Password Hashing with BCrypt
What is BCrypt?
BCrypt is a password hashing algorithm designed to be slow and computationally intensive, making it resistant to brute-force attacks. It automatically handles salt generation and storage.
What is Password Salt?
A salt is a random string added to a password before hashing to ensure unique hashes even for identical passwords. BCrypt generates and manages salts automatically.
const bcrypt = require('bcrypt');
// Generate a salt and hash password
async function hashPassword(password) {
const COST_FACTOR = 12; // Higher = more secure but slower
try {
const hash = await bcrypt.hash(password, COST_FACTOR);
return hash;
} catch (err) {
console.error('Hashing error:', err);
throw new Error('Password hashing failed');
}
}
// Verify password
async function verifyPassword(password, storedHash) {
try {
return await bcrypt.compare(password, storedHash);
} catch (err) {
console.error('Verification error:', err);
throw new Error('Password verification failed');
}
}
Secure Password Validation
This regex ensures passwords meet the following industry standard security requirements:
Minimum of 8 characters
Maximum of 128 characters
At least one capital letter
At least one lowercase letter
At least one special character
At least one number
const PASSWORD_REGEX =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,128}$/;
function isPasswordSecure(password) {
if (!PASSWORD_REGEX.test(password)) {
return {
isValid: false,
reason:
'Password must contain: 8-128 characters, uppercase, lowercase, number, special character',
};
}
// Check against common passwords
const commonPasswords = ['Password123!', 'Admin123!' /* ... */];
if (commonPasswords.includes(password)) {
return {
isValid: false,
reason: 'Password is too common',
};
}
return { isValid: true };
}
Express Security Setup
Security Headers
The following headers ensure that requests from your web service meet industry standard security standards. Implementing them is a quick and easy way to ensure browsers do everything they can to secure your website.
const express = require('express');
const app = express();
// Security middleware
function securityHeaders(req, res, next) {
// Prevents browsers from interpreting files as a different MIME type
res.setHeader('X-Content-Type-Options', 'nosniff');
// Prevents your site from being embedded in iframes
res.setHeader('X-Frame-Options', 'DENY');
// Enables XSS filtering. If a XSS attack is detected, the browser will sanitize the page
res.setHeader('X-XSS-Protection', '1; mode=block');
// Forces browsers to use HTTPS for a specified period
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains',
);
// Controls how much information the browser includes with navigations away from your site
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Helps prevent XSS attacks by restricting sources of content
res.setHeader('Content-Security-Policy', "default-src 'self'");
next();
}
app.use(securityHeaders);
Rate Limiting Implementation
Rate limiting is a simple way to prevent malicious attacks on parts of your application that could be vulnerable to a denial of service style attack. For instance, a denial of service style attack on a route that generates hashed passwords could consume all CPU resources - leaving your application starved of resources.
What follows is a simple in-memory rate limiter. While somewhat suitable for long-running services, using a datastore to store rate limiting information is far more effective and should be done.
class RateLimiter {
constructor(windowMs = 15 * 60 * 1000, maxRequests = 100) {
this.windowMs = windowMs;
this.maxRequests = maxRequests;
this.requests = new Map();
}
cleanup() {
const now = Date.now();
for (const [key, timestamp] of this.requests) {
if (now - timestamp.timestamp > this.windowMs) {
this.requests.delete(key);
}
}
}
middleware() {
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
this.cleanup();
const requestData = this.requests.get(key) || {
count: 0,
timestamp: now,
};
if (now - requestData.timestamp > this.windowMs) {
requestData.count = 0;
requestData.timestamp = now;
}
requestData.count++;
this.requests.set(key, requestData);
if (requestData.count > this.maxRequests) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(
(requestData.timestamp + this.windowMs - now) / 1000,
),
});
}
next();
};
}
}
// Usage
const limiter = new RateLimiter(15 * 60 * 1000, 100);
app.use('/api/', limiter.middleware());
JWTs And You
A JSON Web Token (JWT) is a compact, URL-safe way of representing claims between two parties. Think of it as a signed digital passport that proves the identity and permissions of its bearer.
Structure
A JWT consists of three parts, separated by dots:
Header: Specifies token type and signing algorithm
Payload: Contains the claims (data)
Signature: Verifies the token hasn't been tampered with
Generating Secure JWT Secret
# Generate a 256-bit (32-byte) random secret
node -e "console.log(require('crypto').randomBytes(32).toString('base64'));"
Example JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2OTg0MzE2MDB9.q3DXrw7cZX4uhhbZHHNdqxkP8Sd6rZzOkwq0JTZr8uY
When to Use JWTs
JWTs are ideal for the following:
Authentication: After login, client stores JWT and sends it with subsequent requests
Information Exchange: Securely transmitting data between parties
Stateless Authorization: Server can verify user without a session database
Common Use Cases
Single Sign-On (SSO)
API Authentication
Mobile App Authentication
Cross-Service Communication
Practical Implementation
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
// In production, use a secure environment variable
const JWT_SECRET = 'your-secret-key';
// Middleware to verify JWT
const authenticateToken = (req, res, next) => {
// Get token from Authorization header
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
// Attach user to request object
req.user = user;
next();
});
};
// Login route - creates and sends JWT
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// In reality, you would verify credentials against a database
if (username === 'admin' && password === 'password') {
// Create token with user data (claims)
const token = jwt.sign(
{
userId: '123456',
username: username,
role: 'admin',
},
JWT_SECRET,
{
expiresIn: '24h', // Token expires in 24 hours
issuer: 'your-app-name',
audience: 'your-app-name',
subject: username,
},
);
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
// Protected route - requires valid JWT
app.get('/protected', authenticateToken, (req, res) => {
res.json({
message: 'Access granted!',
user: req.user,
});
});
// Refresh token route
app.post('/refresh-token', authenticateToken, (req, res) => {
// Create new token with updated expiry
const newToken = jwt.sign({ ...req.user }, JWT_SECRET, { expiresIn: '24h' });
res.json({ token: newToken });
});
// Example of role-based authorization
const requireRole = (role) => {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Admin-only route
app.get('/admin', authenticateToken, requireRole('admin'), (req, res) => {
res.json({ message: 'Admin access granted!' });
});
// Error handling for JWT verification failures
app.use((err, req, res, next) => {
if (err.name === 'JsonWebTokenError') {
return res.status(403).json({ error: 'Invalid token' });
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
next(err);
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Sending A JWT From The Client
// Making authenticated requests
async function fetchProtectedResource(token) {
const response = await fetch('http://localhost:3000/protected', {
headers: {
'Authorization': `Bearer ${token}` // token from login
}
});
return response.json();
}
// Login example
async function login(username, password) {
const response = await fetch('http://localhost:3000/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const { token } = await response.json();
return token;
}
Best Practices
Security
Use Strong Secrets: Generate secure, random secrets for signing
Set Appropriate Expiry: Short-lived tokens (24h or less)
Include Essential Claims Only: Minimize sensitive data in payload
Use HTTPS: Always transmit tokens over secure connections
Implementation
Handle Errors Gracefully: Proper error handling for invalid/expired tokens
Refresh Tokens: Implement token refresh strategy
Secure Storage: Store tokens securely (httpOnly cookies for web apps)
Blacklisting: Implement token blacklist for revoked tokens if needed
Common Claims
iss
(issuer): Who issued the tokensub
(subject): Who the token refers toaud
(audience): Who the token is intended forexp
(expiration time): When the token expiresiat
(issued at): When the token was issuedjti
(JWT ID): Unique identifier for the token
Common Pitfalls to Avoid
Storing sensitive data in tokens
Using weak secrets
Not handling token expiration
Storing tokens insecurely
Not validating token signature
Using tokens for long-term sessions
Common Attacks & Mitigations
SQL Injection
Attack: Inserting malicious SQL code via user input
Defense: Use parameterised queries
// ❌ Vulnerable
const query = `SELECT * FROM users WHERE username = '${username}'`;
// ✅ Safe
const query = 'SELECT * FROM users WHERE username = ?';
const [rows] = await connection.execute(query, [username]);
Cross-Site Scripting (XSS)
Attack: Injecting malicious scripts into web pages
Defense: Sanitize input/output
below is an example of how to sanitse HTML for characters or strings attempting to insert malicious code in your browser. In reality, you should use a third party library for sanitisation
function sanitizeHTML(string) {
return string
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
CSRF (Cross-Site Request Forgery)
Attack: Tricking users into performing unwanted actions
Defense: CSRF tokens and SameSite cookies
const crypto = require('crypto');
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
// Middleware to verify CSRF token
function validateCSRF(req, res, next) {
if (req.method === 'POST') {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
}
next();
}
Monitoring And Tracking Of Security Events
Creating an audit trail of security events is important as it allows you to detect security breaches in real-time, reconstruct attack timelines and understand attack patterns and methods. Tracking these events are simple and can be done with a logging library.
Below is an example of what you’d want to track in your web application:
const pino = require('pino');
const logger = pino({
level: 'info',
redact: ['password', 'token'], // Automatically redact sensitive fields
serializers: {
error: pino.stdSerializers.err,
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
},
timestamp: () => `,"time":"${new Date().toISOString()}"`,
});
// Security event logging
function logSecurityEvent(event, data) {
logger.info({
type: 'security',
event,
...data,
timestamp: new Date(),
ip: req.ip,
userAgent: req.headers['user-agent'],
});
}
// Usage
app.post('/login', async (req, res) => {
try {
const user = await authenticateUser(req.body);
logSecurityEvent('login_success', {
username: user.username,
userId: user.id,
});
} catch (err) {
logSecurityEvent('login_failure', {
username: req.body.username,
reason: err.message,
});
}
});
Session Management
What is Session Management?
Session management is the process of securely maintaining a user's state and identity across multiple requests to a web application. Think of it like a wristband at a festival:
The wristband proves you've paid (authenticated)
Gives you access to specific areas (authorised)
Has a limited duration (session expiry)
Can be revoked if needed (session invalidation)
Why Do We Need Sessions?
HTTP is stateless, meaning each request is independent and knows nothing about previous requests. But modern web applications need to:
Remember who users are
Maintain user preferences
Implement shopping carts
Manage authentication state
Control access to protected resources
const session = require('express-session');
const crypto = require('crypto');
function generateSessionId() {
return crypto.randomBytes(32).toString('hex');
}
app.use(
session({
genid: generateSessionId,
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
name: 'sessionId', // Custom cookie name
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000, // 1 hour
},
}),
);
// Session rotation on privilege level change
function rotateSession(req, res, next) {
const oldSession = req.session;
req.session.regenerate((err) => {
if (err) return next(err);
// Copy old session data to new session
Object.assign(req.session, oldSession);
next();
});
}
Want to Learn More?
This cheat sheet is part of our comprehensive Authentication Security Series. Free subscribers to this SubStack gain access to:
Detailed implementation guides
Interactive code examples
Video tutorials
Security best practices
Case studies
Community support