Building secure applications from scratch

Security Best Practices intermediate 11 min read

Who This Is For:

Full-stack developers security engineers DevOps engineers

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

  1. Define security requirements and threat model
  2. Choose security frameworks and libraries
  3. Design authentication and authorization strategy
  4. Plan data protection and encryption approach

Step 2: Implement Authentication System

  1. Set up secure password hashing
  2. Implement JWT token management
  3. Add multi-factor authentication
  4. Create session management

Step 3: Build Authorization Framework

  1. Design role-based access control
  2. Implement permission checking
  3. Create authorization middleware
  4. Test access control scenarios

Step 4: Add Data Protection

  1. Implement field-level encryption
  2. Set up secure key management
  3. Add data anonymization
  4. Configure backup encryption

Step 5: Input Validation and Sanitization

  1. Create validation schemas
  2. Implement sanitization functions
  3. Add file upload security
  4. Test injection prevention

Step 6: Security Monitoring

  1. Set up security logging
  2. Implement intrusion detection
  3. Add vulnerability scanning
  4. 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

Authentication & API Security

Data Protection & Security Principles

Web Security Vulnerabilities

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.

Related Topics

Need Help With Implementation?

While these steps provide a solid foundation, proper implementation often requires expertise and experience.

Get Free Consultation