Complete Guide to Securing API Endpoints

Secure your API endpoints with proper authentication, authorization, encryption, and input validation. Complete guide with implementation examples.

TRY NANO BANANA FOR FREE

Complete Guide to Securing API Endpoints

TRY NANO BANANA FOR FREE
Contents

Every API endpoint is a potential attack vector. Unsecured endpoints lead to data breaches, unauthorized access, and system compromise.

Here's how to secure every endpoint in your API.

Layer 1: Transport Security

Always Use HTTPS

HTTP transmits data in plain text. Anyone on the network can read it.

Enforce HTTPS:

app.use((req, res, next) => {
  if (req.protocol !== 'https' && process.env.NODE_ENV === 'production') {
    return res.redirect(301, `https://${req.hostname}${req.url}`);
  }
  next();
});

Set HSTS Header:

app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

This tells browsers to always use HTTPS for your domain.

Use TLS 1.2 or Higher

Disable older TLS versions:

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('private-key.pem'),
  cert: fs.readFileSync('certificate.pem'),
  minVersion: 'TLSv1.2', // Minimum TLS 1.2
  ciphers: [
    'ECDHE-ECDSA-AES128-GCM-SHA256',
    'ECDHE-RSA-AES128-GCM-SHA256',
    'ECDHE-ECDSA-AES256-GCM-SHA384',
    'ECDHE-RSA-AES256-GCM-SHA384'
  ].join(':')
};

https.createServer(options, app).listen(443);

Layer 2: Authentication

Require Authentication

Every endpoint should verify the caller's identity:

async function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({
      error: 'Authentication required',
      message: 'Provide credentials in the Authorization header'
    });
  }

  try {
    const token = authHeader.replace('Bearer ', '');
    const decoded = jwt.verify(token, JWT_SECRET);

    req.auth = {
      userId: decoded.userId,
      scopes: decoded.scopes || []
    };

    next();
  } catch (error) {
    return res.status(401).json({
      error: 'Invalid token',
      message: error.message
    });
  }
}

// Apply to all routes
app.use('/v1/', authenticate);

Public Endpoints

If you have public endpoints, be explicit:

// Public endpoint (no auth required)
app.get('/v1/public/pets', async (req, res) => {
  const pets = await getPublicPets();
  res.json(pets);
});

// Protected endpoint (auth required)
app.get('/v1/pets', authenticate, async (req, res) => {
  const pets = await getUserPets(req.auth.userId);
  res.json(pets);
});

Layer 3: Authorization

Authentication tells you who the user is. Authorization tells you what they can do.

Check Resource Ownership

app.get('/v1/pets/:id', authenticate, async (req, res) => {
  const pet = await db.pets.findById(req.params.id);

  if (!pet) {
    return res.status(404).json({ error: 'Pet not found' });
  }

  // Verify ownership
  if (pet.ownerId !== req.auth.userId) {
    return res.status(403).json({
      error: 'Access denied',
      message: 'You do not own this pet'
    });
  }

  res.json(pet);
});

Check Permissions (Scopes)

function requireScopes(...requiredScopes) {
  return (req, res, next) => {
    const userScopes = req.auth.scopes || [];

    const hasAllScopes = requiredScopes.every(scope =>
      userScopes.includes(scope)
    );

    if (!hasAllScopes) {
      return res.status(403).json({
        error: 'Insufficient permissions',
        required: requiredScopes,
        granted: userScopes
      });
    }

    next();
  };
}

// Usage
app.post('/v1/pets', authenticate, requireScopes('write:pets'), async (req, res) => {
  // Only users with write:pets scope can create pets
});

app.delete('/v1/pets/:id', authenticate, requireScopes('delete:pets'), async (req, res) => {
  // Only users with delete:pets scope can delete pets
});

Role-Based Access Control

function requireRole(...allowedRoles) {
  return async (req, res, next) => {
    const user = await db.users.findById(req.auth.userId);

    if (!allowedRoles.includes(user.role)) {
      return res.status(403).json({
        error: 'Access denied',
        message: `This endpoint requires one of: ${allowedRoles.join(', ')}`
      });
    }

    next();
  };
}

// Admin-only endpoint
app.delete('/v1/admin/users/:id', authenticate, requireRole('admin'), async (req, res) => {
  await db.users.delete(req.params.id);
  res.status(204).send();
});

Layer 4: Input Validation

Validate All Input

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

app.post('/v1/pets',
  authenticate,
  requireScopes('write:pets'),
  // Validate request body
  body('name').isString().trim().isLength({ min: 1, max: 100 }),
  body('species').isIn(['DOG', 'CAT', 'BIRD', 'RABBIT']),
  body('breed').optional().isString().trim().isLength({ max: 100 }),
  body('age').optional().isInt({ min: 0, max: 30 }),
  body('weight').optional().isFloat({ min: 0, max: 500 }),
  async (req, res) => {
    const errors = validationResult(req);

    if (!errors.isEmpty()) {
      return res.status(422).json({
        error: 'Validation failed',
        details: errors.array()
      });
    }

    const pet = await createPet(req.auth.userId, req.body);
    res.status(201).json(pet);
  }
);

Sanitize Input

const sanitizeHtml = require('sanitize-html');

function sanitizeInput(data) {
  if (typeof data === 'string') {
    return sanitizeHtml(data, {
      allowedTags: [],
      allowedAttributes: {}
    });
  }

  if (Array.isArray(data)) {
    return data.map(sanitizeInput);
  }

  if (typeof data === 'object' && data !== null) {
    const sanitized = {};
    for (const [key, value] of Object.entries(data)) {
      sanitized[key] = sanitizeInput(value);
    }
    return sanitized;
  }

  return data;
}

app.use(express.json());
app.use((req, res, next) => {
  if (req.body) {
    req.body = sanitizeInput(req.body);
  }
  next();
});

Layer 5: Rate Limiting

Global Rate Limit

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

const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 1000, // 1000 requests per window
  message: { error: 'Too many requests' }
});

app.use('/v1/', globalLimiter);

Endpoint-Specific Limits

const strictLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true
});

app.post('/v1/auth/login', strictLimiter, async (req, res) => {
  // Only 5 failed login attempts per 15 minutes
});

Layer 6: Output Filtering

Remove Sensitive Fields

function sanitizeUser(user) {
  const { passwordHash, apiKeys, internalNotes, ...safeUser } = user;
  return safeUser;
}

app.get('/v1/users/:id', authenticate, async (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.json(sanitizeUser(user));
});

Field-Level Permissions

function filterFields(data, allowedFields) {
  const filtered = {};
  allowedFields.forEach(field => {
    if (data[field] !== undefined) {
      filtered[field] = data[field];
    }
  });
  return filtered;
}

app.get('/v1/users/:id', authenticate, async (req, res) => {
  const user = await db.users.findById(req.params.id);

  const isOwnProfile = user.id === req.auth.userId;
  const isAdmin = req.auth.scopes.includes('admin:all');

  let allowedFields;
  if (isAdmin) {
    allowedFields = ['id', 'name', 'email', 'role', 'createdAt', 'lastLoginAt'];
  } else if (isOwnProfile) {
    allowedFields = ['id', 'name', 'email', 'createdAt'];
  } else {
    allowedFields = ['id', 'name'];
  }

  res.json(filterFields(user, allowedFields));
});

Layer 7: Security Headers

Set Security Headers

const helmet = require('helmet');

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

// Additional 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');
  next();
});

Layer 8: Logging and Monitoring

Log Security Events

function logSecurityEvent(event, req, details = {}) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    event: event,
    ip: req.ip,
    userId: req.auth?.userId,
    endpoint: req.path,
    method: req.method,
    userAgent: req.headers['user-agent'],
    ...details
  }));
}

// Log authentication failures
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    logSecurityEvent('AUTH_FAILED', req, { reason: err.message });
  }
  next(err);
});

// Log authorization failures
function requireScopes(...scopes) {
  return (req, res, next) => {
    if (!hasScopes(req.auth, scopes)) {
      logSecurityEvent('AUTHZ_FAILED', req, { requiredScopes: scopes });
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

Monitor Suspicious Activity

async function detectSuspiciousActivity(req) {
  const userId = req.auth?.userId || req.ip;
  const key = `suspicious:${userId}`;

  // Track failed attempts
  const failedAttempts = await redis.incr(key);
  await redis.expire(key, 3600); // 1 hour window

  if (failedAttempts > 10) {
    logSecurityEvent('SUSPICIOUS_ACTIVITY', req, {
      failedAttempts: failedAttempts,
      action: 'BLOCKED'
    });

    return true; // Block request
  }

  return false;
}

Complete Endpoint Security Example

app.post('/v1/pets',
  // Layer 1: HTTPS (enforced globally)
  // Layer 2: Authentication
  authenticate,
  // Layer 3: Authorization
  requireScopes('write:pets'),
  // Layer 4: Input validation
  body('name').isString().trim().isLength({ min: 1, max: 100 }),
  body('species').isIn(['DOG', 'CAT', 'BIRD', 'RABBIT']),
  // Layer 5: Rate limiting
  rateLimit({ windowMs: 60000, max: 10 }),
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }

    try {
      const pet = await createPet(req.auth.userId, req.body);

      // Layer 6: Output filtering
      const safePet = sanitizePet(pet);

      // Layer 8: Logging
      logSecurityEvent('PET_CREATED', req, { petId: pet.id });

      res.status(201).json(safePet);
    } catch (error) {
      logSecurityEvent('PET_CREATE_FAILED', req, { error: error.message });
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

Security Checklist

  • ✓ HTTPS enforced
  • ✓ Authentication required
  • ✓ Authorization checked
  • ✓ Input validated
  • ✓ Input sanitized
  • ✓ Rate limiting applied
  • ✓ Output filtered
  • ✓ Security headers set
  • ✓ Events logged
  • ✓ Errors handled safely

Every endpoint should pass this checklist.

Conclusion

Securing API endpoints requires multiple layers of defense. No single technique is enough. Combine authentication, authorization, validation, rate limiting, and monitoring for comprehensive security.

Start with the basics (HTTPS, authentication) and add layers progressively. Test each layer to ensure it works correctly.

Security is not a feature you add at the end—it's a foundation you build from the start.