Building secure applications from scratch
Quick Summary (TL;DR)
Building secure applications requires implementing security from the foundation up, not as an afterthought. This comprehensive guide covers essential security practices including secure authentication and authorization, data protection, input validation, secure coding practices, and vulnerability prevention. Learn how to architect security into your applications, implement defense-in-depth strategies, and create robust security controls that protect against modern threats.
Key Takeaways
- Security by Design: Integrate security considerations from the initial architecture phase
- Defense in Depth: Implement multiple layers of security controls
- Zero Trust Architecture: Never trust, always verify every request and user
- Secure Authentication: Implement robust authentication with MFA and secure session management
- Data Protection: Encrypt data at rest and in transit with proper key management
- Input Validation: Validate and sanitize all user inputs to prevent injection attacks
- Security Monitoring: Implement comprehensive logging and monitoring for threat detection
The Solution
Building secure applications requires a systematic approach that addresses security at every layer of your application stack. Here’s how to implement comprehensive security from the ground up:
1. Secure Architecture Foundation
Security Configuration Setup:
// security/SecurityConfig.ts
export const securityConfig = {
authentication: {
jwtSecret: process.env.JWT_SECRET!,
jwtExpiration: '15m',
refreshTokenExpiration: '7d',
mfaRequired: process.env.NODE_ENV === 'production',
sessionTimeout: 30 * 60 * 1000,
},
encryption: {
algorithm: 'aes-256-gcm',
saltRounds: 12,
},
rateLimit: {
windowMs: 15 * 60 * 1000, // 15 minutes
maxRequests: 100,
},
};
Essential Security Middleware:
// middleware/SecurityMiddleware.ts
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import cors from 'cors';
// Security headers
export const securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
hsts: { maxAge: 31536000, includeSubDomains: true },
});
// Rate limiting
export const rateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests, please try again later.',
});
// Input sanitization
export const sanitizeInput = (req, res, next) => {
const sanitize = (obj) => {
if (typeof obj === 'string') {
return obj.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '').replace(/javascript:/gi, '');
}
return obj;
};
req.body = sanitize(req.body);
next();
};
2. Robust Authentication System
JWT Authentication Implementation:
// services/AuthenticationService.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
export class AuthenticationService {
private readonly jwtSecret = process.env.JWT_SECRET!;
private readonly jwtExpiration = '15m';
async authenticateUser(email: string, password: string): Promise<any> {
// Find user and verify password
const user = await this.findUserByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
throw new Error('INVALID_CREDENTIALS');
}
// Generate tokens
return this.generateTokens(user);
}
async generateTokens(user: any) {
const payload = {
userId: user.id,
email: user.email,
roles: user.roles,
};
const accessToken = jwt.sign(payload, this.jwtSecret, {
expiresIn: this.jwtExpiration,
});
const refreshToken = crypto.randomBytes(64).toString('hex');
await this.storeRefreshToken(user.id, refreshToken);
return { accessToken, refreshToken };
}
async verifyToken(token: string) {
try {
return jwt.verify(token, this.jwtSecret);
} catch (error) {
throw new Error('INVALID_TOKEN');
}
}
}
Multi-Factor Authentication Implementation:
// auth/AuthenticationService.ts
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
import { User } from '../models/User';
import { securityConfig } from '../security/SecurityConfig';
export class AuthenticationService {
async registerUser(userData: {
email: string;
password: string;
name: string;
}): Promise<{ user: User; tokens: AuthTokens }> {
// Validate password strength
this.validatePasswordStrength(userData.password);
// Check if user already exists
const existingUser = await User.findOne({ email: userData.email });
if (existingUser) {
throw new Error('User already exists');
}
// Hash password
const hashedPassword = await bcrypt.hash(userData.password, securityConfig.encryption.saltRounds);
// Generate MFA secret
const mfaSecret = speakeasy.generateSecret({
name: `SecureApp (${userData.email})`,
issuer: 'SecureApp',
});
// Create user
const user = await User.create({
email: userData.email,
password: hashedPassword,
name: userData.name,
mfaSecret: mfaSecret.base32,
mfaEnabled: false,
emailVerified: false,
loginAttempts: 0,
lockUntil: null,
});
// Generate tokens
const tokens = await this.generateTokens(user);
// Send verification email
await this.sendVerificationEmail(user);
return { user: this.sanitizeUser(user), tokens };
}
async authenticateUser(credentials: {
email: string;
password: string;
mfaToken?: string;
}): Promise<{ user: User; tokens: AuthTokens }> {
const user = await User.findOne({ email: credentials.email });
if (!user) {
throw new Error('Invalid credentials');
}
// Check if account is locked
if (user.lockUntil && user.lockUntil > new Date()) {
throw new Error('Account temporarily locked due to too many failed attempts');
}
// Verify password
const isPasswordValid = await bcrypt.compare(credentials.password, user.password);
if (!isPasswordValid) {
await this.handleFailedLogin(user);
throw new Error('Invalid credentials');
}
// Verify MFA if enabled
if (user.mfaEnabled) {
if (!credentials.mfaToken) {
throw new Error('MFA token required');
}
const isMfaValid = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token: credentials.mfaToken,
window: 2,
});
if (!isMfaValid) {
await this.handleFailedLogin(user);
throw new Error('Invalid MFA token');
}
}
// Reset login attempts on successful login
await User.updateOne({ _id: user._id }, { $unset: { loginAttempts: 1, lockUntil: 1 } });
// Update last login
user.lastLogin = new Date();
await user.save();
// Generate tokens
const tokens = await this.generateTokens(user);
return { user: this.sanitizeUser(user), tokens };
}
async setupMFA(userId: string): Promise<{ qrCode: string; backupCodes: string[] }> {
const user = await User.findById(userId);
if (!user) {
throw new Error('User not found');
}
// Generate QR code for MFA setup
const qrCodeUrl = speakeasy.otpauthURL({
secret: user.mfaSecret,
label: user.email,
issuer: 'SecureApp',
encoding: 'base32',
});
const qrCode = await QRCode.toDataURL(qrCodeUrl);
// Generate backup codes
const backupCodes = Array.from({ length: 10 }, () => Math.random().toString(36).substring(2, 10).toUpperCase());
// Hash and store backup codes
const hashedBackupCodes = await Promise.all(backupCodes.map((code) => bcrypt.hash(code, 10)));
user.backupCodes = hashedBackupCodes;
await user.save();
return { qrCode, backupCodes };
}
private async generateTokens(user: User): Promise<AuthTokens> {
const payload = {
userId: user._id,
email: user.email,
role: user.role,
};
const accessToken = jwt.sign(payload, securityConfig.authentication.jwtSecret, {
expiresIn: securityConfig.authentication.jwtExpiration,
});
const refreshToken = jwt.sign({ userId: user._id }, securityConfig.authentication.jwtSecret, {
expiresIn: securityConfig.authentication.refreshTokenExpiration,
});
// Store refresh token hash in database
const refreshTokenHash = await bcrypt.hash(refreshToken, 10);
await User.updateOne({ _id: user._id }, { refreshToken: refreshTokenHash });
return { accessToken, refreshToken };
}
private async handleFailedLogin(user: User): Promise<void> {
const maxAttempts = 5;
const lockTime = 30 * 60 * 1000; // 30 minutes
user.loginAttempts = (user.loginAttempts || 0) + 1;
if (user.loginAttempts >= maxAttempts) {
user.lockUntil = new Date(Date.now() + lockTime);
}
await user.save();
}
private validatePasswordStrength(password: string): void {
const policy = securityConfig.authentication.passwordPolicy;
if (password.length < policy.minLength) {
throw new Error(`Password must be at least ${policy.minLength} characters long`);
}
if (policy.requireUppercase && !/[A-Z]/.test(password)) {
throw new Error('Password must contain at least one uppercase letter');
}
if (policy.requireLowercase && !/[a-z]/.test(password)) {
throw new Error('Password must contain at least one lowercase letter');
}
if (policy.requireNumbers && !/\d/.test(password)) {
throw new Error('Password must contain at least one number');
}
if (policy.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
throw new Error('Password must contain at least one special character');
}
// Check against common passwords
if (policy.preventCommonPasswords) {
const commonPasswords = ['password', '123456', 'qwerty', 'admin'];
if (commonPasswords.includes(password.toLowerCase())) {
throw new Error('Password is too common');
}
}
}
private sanitizeUser(user: User): any {
const { password, mfaSecret, refreshToken, backupCodes, ...sanitized } = user.toObject();
return sanitized;
}
}
interface AuthTokens {
accessToken: string;
refreshToken: string;
}
3. Authorization and Access Control
Role-Based Access Control (RBAC):
// services/AuthorizationService.ts
export class AuthorizationService {
hasPermission(user: any, requiredPermission: string): boolean {
// Check direct permissions
if (user.permissions?.includes(requiredPermission)) {
return true;
}
// Check role-based permissions
return user.roles?.some((role: any) => role.permissions?.includes(requiredPermission));
}
hasRole(user: any, requiredRoles: string | string[]): boolean {
const roles = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
return user.roles?.some((userRole: any) => roles.includes(userRole.name));
}
canAccessResource(user: any, resource: any): boolean {
// Check if user owns the resource
return resource.userId === user.id || this.hasRole(user, ['admin', 'moderator']);
}
// Express middleware for permission checking
requirePermission(permission: string) {
return (req: any, res: any, next: any) => {
if (!req.user || !this.hasPermission(req.user, permission)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
requireRole(roles: string | string[]) {
return (req: any, res: any, next: any) => {
if (!req.user || !this.hasRole(req.user, roles)) {
return res.status(403).json({ error: 'Insufficient role' });
}
next();
};
}
}
4. Data Protection and Encryption
Data Encryption Service:
// services/EncryptionService.ts
import crypto from 'crypto';
import bcrypt from 'bcrypt';
export class EncryptionService {
private readonly algorithm = 'aes-256-gcm';
private readonly saltRounds = 12;
encrypt(data: string, key: Buffer): any {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.algorithm, key, { iv });
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
encrypted,
iv: iv.toString('hex'),
tag: cipher.getAuthTag().toString('hex'),
};
}
decrypt(encryptedData: any, key: Buffer): string {
const decipher = crypto.createDecipher(this.algorithm, key, { iv: Buffer.from(encryptedData.iv, 'hex') });
decipher.setAuthTag(Buffer.from(encryptedData.tag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, this.saltRounds);
}
async verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
generateToken(length: number = 32): string {
return crypto.randomBytes(length).toString('hex');
}
}
// Mongoose plugin for field encryption
export function encryptionPlugin(schema: any, options: any) {
const encryptionService = new EncryptionService();
const key = Buffer.from(options.key, 'hex');
schema.pre('save', function (this: any) {
options.fields?.forEach((field: string) => {
if (this[field] && this.isModified(field)) {
this[field] = encryptionService.encrypt(this[field], key);
}
});
});
}
5. Input Validation and Sanitization
Comprehensive Input Validation:
// validation/ValidationService.ts
import Joi from 'joi';
import DOMPurify from 'isomorphic-dompurify';
import { Request, Response, NextFunction } from 'express';
export class ValidationService {
// User registration validation
static userRegistrationSchema = Joi.object({
email: Joi.string()
.email({ tlds: { allow: false } })
.required()
.messages({
'string.email': 'Please provide a valid email address',
'any.required': 'Email is required',
}),
password: Joi.string()
.min(12)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>])/)
.required()
.messages({
'string.min': 'Password must be at least 12 characters long',
'string.pattern.base': 'Password must contain uppercase, lowercase, number, and special character',
'any.required': 'Password is required',
}),
name: Joi.string()
.min(2)
.max(100)
.pattern(/^[a-zA-Z\s]+$/)
.required()
.messages({
'string.min': 'Name must be at least 2 characters long',
'string.max': 'Name cannot exceed 100 characters',
'string.pattern.base': 'Name can only contain letters and spaces',
'any.required': 'Name is required',
}),
age: Joi.number().integer().min(13).max(120).optional(),
phone: Joi.string()
.pattern(/^\+?[1-9]\d{1,14}$/)
.optional()
.messages({
'string.pattern.base': 'Please provide a valid phone number',
}),
});
// Product creation validation
static productSchema = Joi.object({
name: Joi.string().min(1).max(200).required(),
description: Joi.string().max(2000).optional(),
price: Joi.number().positive().precision(2).required(),
category: Joi.string().valid('Electronics', 'Clothing', 'Books', 'Home', 'Sports').required(),
images: Joi.array().items(Joi.string().uri()).max(10).optional(),
inventory: Joi.object({
quantity: Joi.number().integer().min(0).required(),
inStock: Joi.boolean().required(),
}).optional(),
});
// SQL injection prevention
static sanitizeSqlInput(input: string): string {
if (typeof input !== 'string') return input;
// Remove or escape dangerous SQL characters
return input
.replace(/'/g, "''") // Escape single quotes
.replace(/;/g, '') // Remove semicolons
.replace(/--/g, '') // Remove SQL comments
.replace(/\/\*/g, '') // Remove block comment start
.replace(/\*\//g, ''); // Remove block comment end
}
// XSS prevention
static sanitizeHtml(input: string): string {
if (typeof input !== 'string') return input;
// Use DOMPurify to clean HTML
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: [],
});
}
// NoSQL injection prevention
static sanitizeNoSqlInput(input: any): any {
if (typeof input === 'string') {
// Remove MongoDB operators
return input.replace(/\$\w+/g, '');
}
if (typeof input === 'object' && input !== null) {
const sanitized: any = {};
for (const key in input) {
// Skip MongoDB operators
if (!key.startsWith('$')) {
sanitized[key] = this.sanitizeNoSqlInput(input[key]);
}
}
return sanitized;
}
return input;
}
// File upload validation
static validateFileUpload(file: Express.Multer.File): void {
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'text/plain'];
const maxFileSize = 5 * 1024 * 1024; // 5MB
if (!allowedMimeTypes.includes(file.mimetype)) {
throw new Error('File type not allowed');
}
if (file.size > maxFileSize) {
throw new Error('File size exceeds limit');
}
// Check for malicious file content
if (this.containsMaliciousContent(file.buffer)) {
throw new Error('File contains malicious content');
}
}
private static containsMaliciousContent(buffer: Buffer): boolean {
const content = buffer.toString('utf8', 0, Math.min(buffer.length, 1024));
// Check for script tags, PHP code, etc.
const maliciousPatterns = [/<script/i, /<\?php/i, /<%/, /javascript:/i, /vbscript:/i, /data:text\/html/i];
return maliciousPatterns.some((pattern) => pattern.test(content));
}
}
// Validation middleware factory
export const validate = (schema: Joi.ObjectSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
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({
error: 'VALIDATION_ERROR',
message: 'Input validation failed',
details: errors,
});
}
// Replace request body with validated and sanitized data
req.body = value;
next();
};
};
// Sanitization middleware
export const sanitize = (req: Request, res: Response, next: NextFunction) => {
// Sanitize all string inputs
const sanitizeObject = (obj: any): any => {
if (typeof obj === 'string') {
return ValidationService.sanitizeHtml(
ValidationService.sanitizeSqlInput(ValidationService.sanitizeNoSqlInput(obj))
);
}
if (typeof obj === 'object' && obj !== null) {
const sanitized: any = {};
for (const key in obj) {
sanitized[key] = sanitizeObject(obj[key]);
}
return sanitized;
}
return obj;
};
req.body = sanitizeObject(req.body);
req.query = sanitizeObject(req.query);
req.params = sanitizeObject(req.params);
next();
};
Implementation Steps
Step 1: Security Architecture Planning
- Define security requirements and threat model
- Choose security frameworks and libraries
- Design authentication and authorization strategy
- Plan data protection and encryption approach
Step 2: Implement Authentication System
- Set up secure password hashing
- Implement JWT token management
- Add multi-factor authentication
- Create session management
Step 3: Build Authorization Framework
- Design role-based access control
- Implement permission checking
- Create authorization middleware
- Test access control scenarios
Step 4: Add Data Protection
- Implement field-level encryption
- Set up secure key management
- Add data anonymization
- Configure backup encryption
Step 5: Input Validation and Sanitization
- Create validation schemas
- Implement sanitization functions
- Add file upload security
- Test injection prevention
Step 6: Security Monitoring
- Set up security logging
- Implement intrusion detection
- Add vulnerability scanning
- Create incident response procedures
Common Questions
Q: Should I implement my own authentication or use a third-party service? A: For most applications, use established services like Auth0, AWS Cognito, or Firebase Auth. Only implement custom authentication if you have specific requirements and security expertise.
Q: How do I securely store API keys and secrets? A: Use environment variables, secret management services (AWS Secrets Manager, Azure Key Vault), or encrypted configuration files. Never commit secrets to version control.
Q: What’s the difference between authentication and authorization? A: Authentication verifies who you are (login), while authorization determines what you can do (permissions). Both are essential for application security.
Q: How often should I rotate encryption keys? A: Rotate keys regularly based on your security policy - typically every 90 days for high-security applications, annually for standard applications.
Q: Should I encrypt all database fields? A: Encrypt sensitive data like PII, payment information, and secrets. Don’t encrypt searchable fields or data that doesn’t need protection, as it impacts performance.
Tools & Resources
Security Frameworks
- Helmet.js: Security headers for Express
- OWASP ZAP: Security testing tool
- Snyk: Vulnerability scanning
- SonarQube: Code security analysis
Authentication Libraries
- Passport.js: Authentication middleware
- Auth0: Authentication as a service
- Firebase Auth: Google’s authentication service
- AWS Cognito: Amazon’s user management
Encryption Tools
- Node.js Crypto: Built-in encryption
- bcrypt: Password hashing
- jsonwebtoken: JWT implementation
- speakeasy: TOTP/HOTP implementation
Validation Libraries
- Joi: Object schema validation
- express-validator: Express validation middleware
- DOMPurify: HTML sanitization
- validator.js: String validation
Related Topics
Authentication & API Security
- A Guide to API Authentication with OAuth 2.0 and JWTs
- Building Secure APIs from Scratch
- API Security Best Practices
Data Protection & Security Principles
- Best Practices for Securely Storing Passwords
- The Principle of Least Privilege
- Securing Your Supply Chain: A Guide to Managing Open Source Dependencies
Web Security Vulnerabilities
- Understanding Cross-Site Scripting (XSS): A Guide to Prevention
- Preventing Cross-Site Request Forgery (CSRF) Attacks
Security Headers & Policies
Need Help With Implementation?
Building secure applications requires deep security expertise and ongoing vigilance. Our security team specializes in implementing comprehensive security solutions that protect against modern threats.
What we can help with:
- Security architecture design and review
- Authentication and authorization implementation
- Data protection and encryption strategies
- Security testing and vulnerability assessment
- Compliance and regulatory requirements
Contact our security experts to secure your applications from the ground up.