Backend EngineeringSecurityAuthenticationAuthorization

Backend Security Best Practices

Satyam Parmar
January 14, 2025
9 min read

Backend Security Best Practices

Security is paramount in backend development. This comprehensive guide covers essential security practices to protect your applications from common vulnerabilities and attacks.

Authentication & Authorization

JWT Implementation

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// Secure password hashing
async function hashPassword(password) {
  const saltRounds = 12;
  return await bcrypt.hash(password, saltRounds);
}

// Verify password
async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash);
}

// Generate secure JWT
function generateToken(user) {
  const payload = {
    id: user.id,
    email: user.email,
    role: user.role
  };
  
  return jwt.sign(payload, process.env.JWT_SECRET, {
    expiresIn: '15m', // Short expiration
    issuer: 'your-app',
    audience: 'your-app-users'
  });
}

// Generate refresh token
function generateRefreshToken(user) {
  return jwt.sign(
    { id: user.id, type: 'refresh' },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );
}

Java/Spring Boot:

@Service
public class JwtService {
    
    @Value("${jwt.secret}")
    private String secretKey;
    
    @Value("${jwt.expiration}")
    private long jwtExpiration;
    
    @Value("${jwt.refresh-expiration}")
    private long refreshExpiration;
    
    // Secure password hashing with BCrypt
    public String hashPassword(String password) {
        return BCrypt.hashpw(password, BCrypt.gensalt(12));
    }
    
    // Verify password
    public boolean verifyPassword(String password, String hash) {
        return BCrypt.checkpw(password, hash);
    }
    
    // Generate JWT token
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", ((UserPrincipal) userDetails).getId());
        claims.put("email", userDetails.getUsername());
        claims.put("role", userDetails.getAuthorities());
        
        return createToken(claims, userDetails.getUsername(), jwtExpiration);
    }
    
    // Generate refresh token
    public String generateRefreshToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", ((UserPrincipal) userDetails).getId());
        claims.put("type", "refresh");
        
        return createToken(claims, userDetails.getUsername(), refreshExpiration);
    }
    
    private String createToken(Map<String, Object> claims, String subject, long expiration) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .setIssuer("your-app")
                .setAudience("your-app-users")
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
    
    // Validate token
    public boolean validateToken(String token, UserDetails userDetails) {
        try {
            final String username = extractUsername(token);
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        } catch (Exception e) {
            return false;
        }
    }
    
    private String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
    
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
    
    private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
    
    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
    }
    
    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
}

Role-Based Access Control (RBAC)

// Define roles and permissions
const ROLES = {
  ADMIN: 'admin',
  USER: 'user',
  MODERATOR: 'moderator'
};

const PERMISSIONS = {
  READ_USERS: 'read:users',
  WRITE_USERS: 'write:users',
  DELETE_USERS: 'delete:users',
  READ_ORDERS: 'read:orders',
  WRITE_ORDERS: 'write:orders'
};

const ROLE_PERMISSIONS = {
  [ROLES.ADMIN]: [
    PERMISSIONS.READ_USERS,
    PERMISSIONS.WRITE_USERS,
    PERMISSIONS.DELETE_USERS,
    PERMISSIONS.READ_ORDERS,
    PERMISSIONS.WRITE_ORDERS
  ],
  [ROLES.USER]: [
    PERMISSIONS.READ_ORDERS,
    PERMISSIONS.WRITE_ORDERS
  ]
};

// Middleware to check permissions
function requirePermission(permission) {
  return (req, res, next) => {
    const userRole = req.user.role;
    const userPermissions = ROLE_PERMISSIONS[userRole] || [];
    
    if (!userPermissions.includes(permission)) {
      return res.status(403).json({
        error: 'Insufficient permissions'
      });
    }
    
    next();
  };
}

// Usage
app.get('/api/users', 
  authenticateToken,
  requirePermission(PERMISSIONS.READ_USERS),
  getUsers
);

Input Validation & Sanitization

Express Validator

const { body, param, query, validationResult } = require('express-validator');
const { sanitize } = require('express-validator');

// User registration validation
const validateUserRegistration = [
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Valid email is required'),
  
  body('password')
    .isLength({ min: 8 })
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
    .withMessage('Password must be at least 8 characters with uppercase, lowercase, number and special character'),
  
  body('name')
    .trim()
    .isLength({ min: 2, max: 50 })
    .matches(/^[a-zA-Z\s]+$/)
    .withMessage('Name must be 2-50 characters, letters only'),
  
  body('age')
    .optional()
    .isInt({ min: 13, max: 120 })
    .withMessage('Age must be between 13 and 120'),
  
  // Sanitize inputs
  sanitize('email').trim().toLowerCase(),
  sanitize('name').trim(),
  
  // Handle validation errors
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        error: 'Validation failed',
        details: errors.array()
      });
    }
    next();
  }
];

SQL Injection Prevention

// Use parameterized queries
async function getUserById(id) {
  // Good: Parameterized query
  const query = 'SELECT * FROM users WHERE id = $1';
  const result = await pool.query(query, [id]);
  return result.rows[0];
}

// Bad: String concatenation (vulnerable to SQL injection)
// const query = `SELECT * FROM users WHERE id = ${id}`;

// For dynamic queries, use query builder
const { Pool } = require('pg');
const { QueryBuilder } = require('knex');

const db = require('knex')({
  client: 'postgresql',
  connection: process.env.DATABASE_URL
});

// Safe dynamic query building
async function searchUsers(filters) {
  let query = db('users').select('*');
  
  if (filters.name) {
    query = query.where('name', 'ilike', `%${filters.name}%`);
  }
  
  if (filters.email) {
    query = query.where('email', 'ilike', `%${filters.email}%`);
  }
  
  if (filters.status) {
    query = query.where('status', filters.status);
  }
  
  return await query;
}

Rate Limiting & DDoS Protection

Express Rate Limiting

const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');

// General rate limiting
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests from this IP, please try again later.'
  },
  standardHeaders: true,
  legacyHeaders: false
});

// Strict rate limiting for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // limit each IP to 5 requests per windowMs
  skipSuccessfulRequests: true,
  message: {
    error: 'Too many authentication attempts, please try again later.'
  }
});

// Slow down repeated requests
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000, // 15 minutes
  delayAfter: 2, // allow 2 requests per 15 minutes, then...
  delayMs: 500 // begin adding 500ms of delay per request above 2
});

// Apply rate limiting
app.use('/api/', generalLimiter);
app.use('/api/auth/', authLimiter);
app.use('/api/', speedLimiter);

Advanced Rate Limiting

const RedisStore = require('rate-limit-redis');
const redis = require('redis');

const client = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT
});

// Redis-based rate limiting for distributed systems
const redisLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => client.sendCommand(args)
  }),
  windowMs: 15 * 60 * 1000,
  max: 100,
  keyGenerator: (req) => {
    // Custom key generation
    return `${req.ip}:${req.user?.id || 'anonymous'}`;
  }
});

Data Encryption

Sensitive Data Encryption

const crypto = require('crypto');

class EncryptionService {
  constructor() {
    this.algorithm = 'aes-256-gcm';
    this.key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
  }
  
  encrypt(text) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher(this.algorithm, this.key);
    cipher.setAAD(Buffer.from('additional-data'));
    
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    
    const authTag = cipher.getAuthTag();
    
    return {
      encrypted,
      iv: iv.toString('hex'),
      authTag: authTag.toString('hex')
    };
  }
  
  decrypt(encryptedData) {
    const decipher = crypto.createDecipher(
      this.algorithm, 
      this.key
    );
    
    decipher.setAAD(Buffer.from('additional-data'));
    decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
    
    let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    
    return decrypted;
  }
}

// Usage
const encryptionService = new EncryptionService();

// Encrypt sensitive data before storing
const encryptedSSN = encryptionService.encrypt(user.ssn);
await db.query(
  'INSERT INTO users (ssn_encrypted, ssn_iv, ssn_auth_tag) VALUES ($1, $2, $3)',
  [encryptedSSN.encrypted, encryptedSSN.iv, encryptedSSN.authTag]
);

Security Headers

Helmet.js Configuration

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"],
    },
  },
  crossOriginEmbedderPolicy: false,
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// Additional security headers
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
  next();
});

CORS Configuration

const cors = require('cors');

const corsOptions = {
  origin: function (origin, callback) {
    // Allow requests with no origin (mobile apps, Postman, etc.)
    if (!origin) return callback(null, true);
    
    const allowedOrigins = [
      'https://yourdomain.com',
      'https://www.yourdomain.com',
      'https://staging.yourdomain.com'
    ];
    
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  optionsSuccessStatus: 200,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
};

app.use(cors(corsOptions));

Logging & Monitoring

Security Event Logging

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'security.log' }),
    new winston.transports.Console()
  ]
});

// Log security events
function logSecurityEvent(event, details) {
  logger.warn('Security Event', {
    event,
    details,
    timestamp: new Date().toISOString(),
    ip: details.ip,
    userAgent: details.userAgent
  });
}

// Usage
app.post('/api/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await findUserByEmail(email);
    
    if (!user) {
      logSecurityEvent('LOGIN_FAILED', {
        email,
        ip: req.ip,
        userAgent: req.get('User-Agent')
      });
      
      return res.status(401).json({
        error: 'Invalid credentials'
      });
    }
    
    // ... rest of login logic
  } catch (error) {
    logSecurityEvent('LOGIN_ERROR', {
      error: error.message,
      ip: req.ip
    });
    
    res.status(500).json({ error: 'Internal server error' });
  }
});

Environment Security

Secure Environment Configuration

// .env.example
/*
NODE_ENV=production
PORT=3000

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname

# JWT
JWT_SECRET=your-super-secret-jwt-key-here
REFRESH_SECRET=your-super-secret-refresh-key-here

# Encryption
ENCRYPTION_KEY=your-32-character-hex-encryption-key

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password

# External APIs
API_KEY=your-api-key
API_SECRET=your-api-secret
*/

// Validate required environment variables
const requiredEnvVars = [
  'NODE_ENV',
  'DATABASE_URL',
  'JWT_SECRET',
  'REFRESH_SECRET',
  'ENCRYPTION_KEY'
];

function validateEnvironment() {
  const missing = requiredEnvVars.filter(envVar => !process.env[envVar]);
  
  if (missing.length > 0) {
    throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
  }
  
  // Validate JWT secret strength
  if (process.env.JWT_SECRET.length < 32) {
    throw new Error('JWT_SECRET must be at least 32 characters long');
  }
}

validateEnvironment();

API Security Testing

Automated Security Tests

const request = require('supertest');
const app = require('../app');

describe('Security Tests', () => {
  test('should reject requests without authentication', async () => {
    const response = await request(app)
      .get('/api/users')
      .expect(401);
    
    expect(response.body.error).toBe('Access token required');
  });
  
  test('should reject invalid JWT tokens', async () => {
    const response = await request(app)
      .get('/api/users')
      .set('Authorization', 'Bearer invalid-token')
      .expect(403);
    
    expect(response.body.error).toBe('Invalid token');
  });
  
  test('should prevent SQL injection', async () => {
    const maliciousInput = "'; DROP TABLE users; --";
    
    const response = await request(app)
      .get(`/api/users?search=${maliciousInput}`)
      .set('Authorization', 'Bearer valid-token')
      .expect(200);
    
    // Should not crash or return unexpected data
    expect(response.body.success).toBe(true);
  });
  
  test('should enforce rate limiting', async () => {
    const promises = [];
    
    // Make 10 requests quickly
    for (let i = 0; i < 10; i++) {
      promises.push(
        request(app)
          .post('/api/auth/login')
          .send({ email: 'test@example.com', password: 'wrong' })
      );
    }
    
    const responses = await Promise.all(promises);
    
    // Some requests should be rate limited
    const rateLimitedResponses = responses.filter(
      res => res.status === 429
    );
    
    expect(rateLimitedResponses.length).toBeGreaterThan(0);
  });
});

Conclusion

Security is not a one-time implementation but an ongoing process. Key takeaways:

  1. Always validate and sanitize input
  2. Use parameterized queries to prevent SQL injection
  3. Implement proper authentication and authorization
  4. Use HTTPS everywhere
  5. Set security headers
  6. Implement rate limiting
  7. Log security events
  8. Keep dependencies updated
  9. Regular security audits
  10. Follow the principle of least privilege

Remember: Security is everyone's responsibility, and it's better to be proactive than reactive when it comes to protecting your application and users' data.

Related Articles

Home