API Security Best Practices

Advanced35 min readLast updated: April 28, 2025

Introduction to API Security

API security is a critical aspect of modern application development. As applications increasingly rely on APIs to connect services and share data, securing these interfaces becomes essential to protect sensitive information and maintain user trust.

This guide covers best practices for securing your API integrations, from authentication and authorization to data validation and protection against common attacks.

Required Configuration

For your application to work successfully with our APIs, you need to include the following configuration in your bls.toml file:

[deployment]
permission = "public"
nodes = 1

permissions = [ "https://yourapiwebsite/" ]

This configuration grants your application the necessary permissions to access our API endpoints securely.

Understanding API Security Risks

Before diving into best practices, it's important to understand the common security risks associated with API usage:

OWASP API Security Top 10

The Open Web Application Security Project (OWASP) identifies these top API security risks:

  1. Broken Object Level Authorization: Improper access control allowing unauthorized access to data
  2. Broken User Authentication: Flaws in authentication mechanisms
  3. Excessive Data Exposure: APIs returning more data than necessary
  4. Lack of Resources & Rate Limiting: No protection against resource exhaustion
  5. Broken Function Level Authorization: Improper access control for functions
  6. Mass Assignment: Client-provided data binding to internal objects
  7. Security Misconfiguration: Insecure default configurations, incomplete setups
  8. Injection: Untrusted data sent to interpreters
  9. Improper Assets Management: Unpatched systems, outdated documentation
  10. Insufficient Logging & Monitoring: Lack of visibility into suspicious activities

Secure Authentication

Authentication verifies the identity of clients accessing your API. Implementing secure authentication is the first line of defense.

Authentication Best Practices

  • Use HTTPS: Always use HTTPS to encrypt data in transit
  • Implement Strong Authentication: Use OAuth 2.0, JWT, or API keys depending on your needs
  • Secure Token Storage: Store tokens securely and handle them properly
  • Implement Token Expiration: Set reasonable expiration times for tokens
  • Use Refresh Tokens: Implement refresh token rotation for long-lived sessions

Implementation Example

// Secure authentication example
async function authenticateSecurely() {
  try {
    // Use HTTPS for all requests
    const response = await fetch('https://api.example.com/auth', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        username: username,
        password: password
      }),
      // Ensure cookies are sent with the request
      credentials: 'include'
    });
    
    if (!response.ok) {
      throw new Error('Authentication failed: ' + response.status);
    }
    
    const data = await response.json();
    
    // Store token securely
    if (data.token) {
      // For JWT or similar tokens
      // Don't store in localStorage for sensitive applications
      sessionStorage.setItem('auth_token', data.token);
      
      // Set token expiration
      const expiresAt = Date.now() + data.expiresIn * 1000;
      sessionStorage.setItem('token_expires_at', expiresAt.toString());
    }
    
    return data;
  } catch (error) {
    console.error('Authentication error:', error);
    throw error;
  }
}

For more details on authentication methods, see our API Authentication Methods guide.

Authorization

While authentication verifies who you are, authorization determines what you're allowed to do.

Authorization Best Practices

  • Implement Principle of Least Privilege: Grant only the permissions necessary
  • Use Role-Based Access Control (RBAC): Assign permissions based on roles
  • Implement Object-Level Authorization: Verify access rights for specific resources
  • Validate Authorization on Every Request: Don't rely on client-side checks
  • Use Scoped Tokens: Include permission scopes in access tokens

Making Secure API Requests

// Making secure API requests
async function fetchSecureData(url) {
  try {
    // Get token from secure storage
    const token = sessionStorage.getItem('auth_token');
    const tokenExpiresAt = sessionStorage.getItem('token_expires_at');
    
    // Check if token is expired
    if (tokenExpiresAt && Date.now() > parseInt(tokenExpiresAt)) {
      // Token is expired, refresh it
      await refreshToken();
      // Get the new token
      token = sessionStorage.getItem('auth_token');
    }
    
    // Make the request with authorization
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      // Prevent CSRF
      credentials: 'include'
    });
    
    if (!response.ok) {
      // Handle specific error cases
      if (response.status === 401) {
        // Unauthorized - token might be invalid
        sessionStorage.removeItem('auth_token');
        // Redirect to login
        window.location.href = '/login';
        throw new Error('Authentication required');
      }
      
      throw new Error('Request failed: ' + response.status);
    }
    
    return await response.json();
  } catch (error) {
    console.error('Secure request error:', error);
    throw error;
  }
}

Data Validation and Sanitization

Never trust input from clients. Always validate and sanitize data before processing it.

Input Validation Best Practices

  • Validate All Input: Check type, format, length, and range
  • Implement Both Client and Server Validation: Client for UX, server for security
  • Use Whitelisting: Accept only known good input
  • Sanitize Output: Prevent XSS by encoding output
  • Use Parameterized Queries: Prevent SQL injection

Implementation Example

// Client-side input validation
function validateUserInput(input) {
  // Define validation rules
  const validations = {
    username: {
      pattern: /^[a-zA-Z0-9_]{3,20}$/,
      message: 'Username must be 3-20 characters and contain only letters, numbers, and underscores'
    },
    email: {
      pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
      message: 'Please enter a valid email address'
    },
    password: {
      pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
      message: 'Password must be at least 8 characters and include uppercase, lowercase, number, and special character'
    }
  };
  
  const errors = {};
  
  // Validate each field
  Object.keys(input).forEach(field => {
    // Skip fields that don't have validation rules
    if (!validations[field]) return;
    
    const value = input[field];
    const validation = validations[field];
    
    // Check if value matches pattern
    if (!validation.pattern.test(value)) {
      errors[field] = validation.message;
    }
  });
  
  // Sanitize input to prevent XSS
  const sanitized = {};
  Object.keys(input).forEach(field => {
    if (typeof input[field] === 'string') {
      // Basic sanitization - in production use a library like DOMPurify
      sanitized[field] = input[field]
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
    } else {
      sanitized[field] = input[field];
    }
  });
  
  return {
    isValid: Object.keys(errors).length === 0,
    errors,
    sanitized
  };
}

Protecting Against Common Attacks

APIs are vulnerable to various attacks. Here's how to protect against the most common ones:

Cross-Site Request Forgery (CSRF)

CSRF attacks trick users into performing unwanted actions on a site they're authenticated to.

CSRF Protection Strategies

  • Use Anti-CSRF Tokens: Include tokens in forms and requests
  • Check Origin and Referer Headers: Verify requests come from legitimate sources
  • Use SameSite Cookies: Set cookies with SameSite=Strict or Lax

Implementation Example

// CSRF protection example
// This assumes your server provides a CSRF token

// Get CSRF token from a cookie or meta tag
function getCsrfToken() {
  // From meta tag
  const metaTag = document.querySelector('meta[name="csrf-token"]');
  if (metaTag) {
    return metaTag.getAttribute('content');
  }
  
  // From cookie
  const cookies = document.cookie.split(';');
  for (let cookie of cookies) {
    const [name, value] = cookie.trim().split('=');
    if (name === 'XSRF-TOKEN') {
      return decodeURIComponent(value);
    }
  }
  
  return null;
}

// Include CSRF token in requests
async function postWithCsrfProtection(url, data) {
  const csrfToken = getCsrfToken();
  
  if (!csrfToken) {
    throw new Error('CSRF token not found');
  }
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken
    },
    // Include cookies in the request
    credentials: 'include',
    body: JSON.stringify(data)
  });
  
  if (!response.ok) {
    throw new Error('Request failed: ' + response.status);
  }
  
  return response.json();
}

Cross-Site Scripting (XSS)

XSS attacks inject malicious scripts into web pages viewed by other users.

XSS Protection Strategies

  • Sanitize User Input: Encode special characters
  • Use Content Security Policy (CSP): Restrict which scripts can run
  • Use HttpOnly and Secure Flags for Cookies: Prevent JavaScript access to cookies
  • Implement Output Encoding: Encode data before rendering
// Setting up Content Security Policy
// In your HTML head or HTTP headers
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted-cdn.com;">

// Using a library like DOMPurify for sanitization
import DOMPurify from 'dompurify';

function renderUserContent(content) {
  // Sanitize content before rendering
  const sanitized = DOMPurify.sanitize(content);
  document.getElementById('user-content').innerHTML = sanitized;
}

Injection Attacks

Injection attacks insert malicious code into interpreters through untrusted data.

Injection Protection Strategies

  • Use Parameterized Queries: For database operations
  • Validate and Sanitize Input: Filter out malicious patterns
  • Use ORMs: Object-Relational Mappers often include protection
  • Implement Least Privilege: Limit database user permissions
// Example of parameterized query in Node.js with prepared statements
const { Pool } = require('pg');
const pool = new Pool(/* connection config */);

async function getUserById(userId) {
  // Use parameterized query instead of string concatenation
  const query = 'SELECT * FROM users WHERE id = $1';
  const values = [userId];
  
  try {
    const result = await pool.query(query, values);
    return result.rows[0];
  } catch (error) {
    console.error('Database error:', error);
    throw error;
  }
}

Rate Limiting and Resource Protection

Protect your API from abuse and denial-of-service attacks with rate limiting.

Rate Limiting Best Practices

  • Implement Request Rate Limiting: Limit requests per client
  • Add Complexity Limits: Restrict query complexity
  • Set Timeouts: Prevent long-running operations
  • Implement Graceful Degradation: Handle overload conditions
  • Monitor Usage Patterns: Detect and block abusive behavior

For more details on rate limiting, see our Dealing with Rate Limits guide.

Secure Data Handling

Protect sensitive data throughout its lifecycle.

Data Protection Best Practices

  • Classify Data: Identify sensitive information
  • Encrypt Sensitive Data: Both in transit and at rest
  • Implement Proper Key Management: Secure storage and rotation of encryption keys
  • Minimize Data Exposure: Return only necessary data
  • Implement Data Masking: Hide sensitive parts of data
  • Follow Data Protection Regulations: Comply with GDPR, CCPA, etc.
// Example of minimizing data exposure
// Instead of returning the entire user object
async function getUserProfile(userId) {
  const user = await db.users.findById(userId);
  
  // Return only necessary fields, exclude sensitive data
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    // Don't include password, SSN, etc.
  };
}

// Example of data masking
function maskCreditCard(cardNumber) {
  // Only show last 4 digits
  return cardNumber.replace(/\d(?=\d{4})/g, '*');
}

Secure Development Lifecycle

Security should be integrated throughout the development process.

Secure Development Practices

  • Security Requirements: Define security requirements early
  • Threat Modeling: Identify potential threats and vulnerabilities
  • Secure Coding Guidelines: Follow established best practices
  • Code Reviews: Include security in code reviews
  • Security Testing: Perform regular security testing
  • Dependency Management: Keep dependencies updated

Security Testing

  • Static Application Security Testing (SAST): Analyze code for vulnerabilities
  • Dynamic Application Security Testing (DAST): Test running applications
  • Penetration Testing: Simulate attacks to find vulnerabilities
  • Dependency Scanning: Check for vulnerabilities in dependencies
// Example of using npm audit to check dependencies
{
  "scripts": {
    "audit": "npm audit",
    "audit:fix": "npm audit fix",
    "prestart": "npm audit"
  }
}

Logging, Monitoring, and Incident Response

Detect and respond to security incidents effectively.

Logging and Monitoring Best Practices

  • Implement Comprehensive Logging: Log security-relevant events
  • Protect Log Data: Secure access to logs
  • Monitor for Suspicious Activity: Set up alerts for unusual patterns
  • Implement Real-time Monitoring: Detect incidents as they happen
  • Establish Baselines: Know what normal activity looks like

Incident Response

  • Develop an Incident Response Plan: Know what to do when incidents occur
  • Define Roles and Responsibilities: Assign clear responsibilities
  • Practice Response Procedures: Conduct regular drills
  • Learn from Incidents: Improve security based on past incidents
// Example of security logging
function logSecurityEvent(event) {
  const logEntry = {
    timestamp: new Date().toISOString(),
    event: event.type,
    user: event.userId,
    ip: event.ipAddress,
    userAgent: event.userAgent,
    details: event.details,
    severity: event.severity
  };
  
  // Log to secure storage
  secureLogger.log(logEntry);
  
  // Alert on high-severity events
  if (event.severity === 'high') {
    alertSystem.trigger(logEntry);
  }
}

API Security Checklist

Use this checklist to ensure you've covered the essential security aspects of your API integration:

Authentication and Authorization

  • ☐ Use HTTPS for all API communications
  • ☐ Implement proper authentication (OAuth, JWT, API keys)
  • ☐ Set appropriate token expiration
  • ☐ Implement role-based access control
  • ☐ Validate authorization on every request

Data Validation and Protection

  • ☐ Validate all input data
  • ☐ Sanitize output to prevent XSS
  • ☐ Use parameterized queries for database operations
  • ☐ Encrypt sensitive data
  • ☐ Minimize data exposure in responses

Attack Prevention

  • ☐ Implement CSRF protection
  • ☐ Set up Content Security Policy
  • ☐ Use secure cookie flags (HttpOnly, Secure, SameSite)
  • ☐ Implement rate limiting
  • ☐ Set up proper CORS configuration

Monitoring and Response

  • ☐ Implement comprehensive logging
  • ☐ Set up monitoring and alerts
  • ☐ Develop an incident response plan
  • ☐ Regularly review security measures
  • ☐ Keep dependencies updated

Conclusion

API security is a complex and evolving field. By following the best practices outlined in this guide, you can significantly reduce the risk of security incidents and protect your users' data.

Remember that security is not a one-time effort but an ongoing process. Regularly review and update your security measures to address new threats and vulnerabilities.

Further Reading