Security & Performance

API Security

20 min Lesson 10 of 35

Introduction to API Security

Application Programming Interfaces (APIs) are the backbone of modern web applications, enabling communication between different services, mobile apps, and third-party integrations. However, APIs also represent significant security risks if not properly protected. API security involves implementing authentication, authorization, rate limiting, input validation, and monitoring to protect sensitive data and prevent unauthorized access.

Critical Understanding: Every API endpoint is a potential attack vector. Unlike traditional web applications with browser-based security features, APIs must implement comprehensive security measures at the application level.

APIs face unique security challenges including automated attacks, credential stuffing, data scraping, and distributed denial-of-service (DDoS) attacks. A single vulnerable endpoint can compromise your entire system, making API security a critical priority for any modern application.

API Authentication Methods

Authentication verifies the identity of clients accessing your API. Different authentication methods offer varying levels of security and complexity, and choosing the right method depends on your application's requirements and threat model.

<?php
// METHOD 1: API Key Authentication (Simple but less secure)
function authenticateApiKey() {
    $api_key = $_SERVER['HTTP_X_API_KEY'] ?? '';
    
    if (empty($api_key)) {
        http_response_code(401);
        echo json_encode(['error' => 'API key required']);
        exit;
    }
    
    // Validate against database (use prepared statements)
    $pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
    $stmt = $pdo->prepare('SELECT user_id, permissions FROM api_keys WHERE key_hash = ? AND is_active = 1');
    $stmt->execute([hash('sha256', $api_key)]);
    
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$result) {
        http_response_code(401);
        echo json_encode(['error' => 'Invalid API key']);
        exit;
    }
    
    return $result;
}

// METHOD 2: HTTP Basic Authentication
function authenticateBasic() {
    if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
        header('WWW-Authenticate: Basic realm="API"');
        http_response_code(401);
        exit;
    }
    
    $username = $_SERVER['PHP_AUTH_USER'];
    $password = $_SERVER['PHP_AUTH_PW'];
    
    // Verify credentials (use password_verify with hashed passwords)
    // Never store plain text passwords
    return verifyUserCredentials($username, $password);
}

// METHOD 3: Bearer Token Authentication
function authenticateBearerToken() {
    $auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
    
    if (!preg_match('/Bearer\s+(\S+)/', $auth_header, $matches)) {
        http_response_code(401);
        echo json_encode(['error' => 'Bearer token required']);
        exit;
    }
    
    $token = $matches[1];
    return validateAccessToken($token);
}
?>
Security Warning: Never transmit API keys or credentials over unencrypted HTTP connections. Always use HTTPS to protect authentication data in transit.

API keys are simple but should be treated like passwords—store only hashed versions in your database, rotate them regularly, and allow users to revoke compromised keys. Basic authentication is convenient for internal APIs but requires HTTPS. Bearer tokens (used with OAuth and JWT) provide better security for modern applications.

API Keys and Rate Limiting

API keys identify clients, while rate limiting prevents abuse by restricting the number of requests a client can make within a time window. Together, they form a fundamental layer of API protection.

<?php
class RateLimiter {
    private $redis;
    private $max_requests;
    private $window_seconds;
    
    public function __construct($redis, $max_requests = 100, $window_seconds = 3600) {
        $this->redis = $redis;
        $this->max_requests = $max_requests;
        $this->window_seconds = $window_seconds;
    }
    
    public function isAllowed($api_key) {
        $key = "rate_limit:" . $api_key;
        $current = $this->redis->get($key);
        
        if ($current === false) {
            // First request in window
            $this->redis->setex($key, $this->window_seconds, 1);
            return true;
        }
        
        if ($current >= $this->max_requests) {
            return false; // Rate limit exceeded
        }
        
        $this->redis->incr($key);
        return true;
    }
    
    public function getRemainingRequests($api_key) {
        $key = "rate_limit:" . $api_key;
        $current = (int)$this->redis->get($key);
        return max(0, $this->max_requests - $current);
    }
    
    public function getResetTime($api_key) {
        $key = "rate_limit:" . $api_key;
        return time() + $this->redis->ttl($key);
    }
}

// Usage
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$limiter = new RateLimiter($redis, 1000, 3600); // 1000 requests per hour
$api_key = authenticateApiKey();

if (!$limiter->isAllowed($api_key)) {
    header('X-RateLimit-Limit: 1000');
    header('X-RateLimit-Remaining: 0');
    header('X-RateLimit-Reset: ' . $limiter->getResetTime($api_key));
    http_response_code(429);
    echo json_encode(['error' => 'Rate limit exceeded']);
    exit;
}

// Add rate limit headers to response
header('X-RateLimit-Limit: 1000');
header('X-RateLimit-Remaining: ' . $limiter->getRemainingRequests($api_key));
?>
Best Practice: Implement different rate limits for different endpoints based on their resource intensity. Read operations can have higher limits than write operations.

OAuth 2.0 for APIs

OAuth 2.0 is an industry-standard authorization framework that enables secure delegated access. Instead of sharing passwords, users grant limited access to their resources through access tokens. OAuth is ideal for third-party integrations and mobile applications.

<?php
// OAuth 2.0 Authorization Code Flow

// Step 1: Client redirects user to authorization endpoint
function generateAuthorizationUrl($client_id, $redirect_uri, $scope) {
    $state = bin2hex(random_bytes(16)); // CSRF protection
    $_SESSION['oauth_state'] = $state;
    
    $params = http_build_query([
        'response_type' => 'code',
        'client_id' => $client_id,
        'redirect_uri' => $redirect_uri,
        'scope' => $scope,
        'state' => $state,
    ]);
    
    return "https://api.example.com/oauth/authorize?" . $params;
}

// Step 2: Handle callback and exchange code for token
function exchangeCodeForToken($code, $client_id, $client_secret, $redirect_uri) {
    // Verify state parameter (CSRF protection)
    if ($_GET['state'] !== $_SESSION['oauth_state']) {
        die('Invalid state parameter');
    }
    
    $data = [
        'grant_type' => 'authorization_code',
        'code' => $code,
        'client_id' => $client_id,
        'client_secret' => $client_secret,
        'redirect_uri' => $redirect_uri,
    ];
    
    $ch = curl_init('https://api.example.com/oauth/token');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    curl_close($ch);
    
    return json_decode($response, true);
}

// Step 3: Refresh expired access token
function refreshAccessToken($refresh_token, $client_id, $client_secret) {
    $data = [
        'grant_type' => 'refresh_token',
        'refresh_token' => $refresh_token,
        'client_id' => $client_id,
        'client_secret' => $client_secret,
    ];
    
    $ch = curl_init('https://api.example.com/oauth/token');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    curl_close($ch);
    
    return json_decode($response, true);
}
?>
OAuth 2.0 Grant Types: Authorization Code (for web apps), Client Credentials (for server-to-server), Implicit (deprecated), Resource Owner Password (for trusted apps), and Refresh Token (for token renewal).

JWT Security Best Practices

JSON Web Tokens (JWT) are a popular method for securely transmitting information between parties. However, improper JWT implementation can lead to serious security vulnerabilities. Understanding JWT security is critical for modern API development.

<?php
// Secure JWT Implementation
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class JWTHandler {
    private $secret_key;
    private $algorithm = 'HS256';
    private $token_lifetime = 3600; // 1 hour
    
    public function __construct() {
        // Use strong random key (at least 256 bits for HS256)
        $this->secret_key = $_ENV['JWT_SECRET_KEY'];
        
        if (strlen($this->secret_key) < 32) {
            throw new Exception('JWT secret key too short');
        }
    }
    
    public function generateToken($user_id, $permissions = []) {
        $issued_at = time();
        $expiration = $issued_at + $this->token_lifetime;
        
        $payload = [
            'iss' => 'https://api.example.com', // Issuer
            'aud' => 'https://api.example.com', // Audience
            'iat' => $issued_at, // Issued at
            'exp' => $expiration, // Expiration
            'nbf' => $issued_at, // Not before
            'jti' => bin2hex(random_bytes(16)), // JWT ID (unique)
            'sub' => $user_id, // Subject (user ID)
            'permissions' => $permissions,
        ];
        
        return JWT::encode($payload, $this->secret_key, $this->algorithm);
    }
    
    public function validateToken($token) {
        try {
            $decoded = JWT::decode($token, new Key($this->secret_key, $this->algorithm));
            
            // Verify issuer and audience
            if ($decoded->iss !== 'https://api.example.com' ||
                $decoded->aud !== 'https://api.example.com') {
                return false;
            }
            
            // Check token revocation (implement token blacklist)
            if ($this->isTokenRevoked($decoded->jti)) {
                return false;
            }
            
            return $decoded;
        } catch (Exception $e) {
            // Token invalid, expired, or malformed
            return false;
        }
    }
    
    private function isTokenRevoked($jti) {
        // Check Redis or database for revoked tokens
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
        return $redis->exists("revoked_token:" . $jti);
    }
    
    public function revokeToken($jti, $expiration) {
        // Add token to blacklist until expiration
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
        $ttl = $expiration - time();
        if ($ttl > 0) {
            $redis->setex("revoked_token:" . $jti, $ttl, 1);
        }
    }
}
?>
JWT Security Risks: Never store sensitive data in JWT payload (it's base64 encoded, not encrypted). Always validate the algorithm to prevent "none" algorithm attacks. Use strong secret keys and implement token revocation.

CORS Configuration

Cross-Origin Resource Sharing (CORS) controls which domains can access your API from browsers. Proper CORS configuration is essential for API security while enabling legitimate cross-origin requests.

<?php
class CORSHandler {
    private $allowed_origins = [
        'https://example.com',
        'https://app.example.com',
    ];
    
    private $allowed_methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
    private $allowed_headers = ['Content-Type', 'Authorization', 'X-API-Key'];
    private $max_age = 86400; // 24 hours
    
    public function handleRequest() {
        $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
        
        // Validate origin against whitelist
        if (!in_array($origin, $this->allowed_origins, true)) {
            // For development, you might allow localhost
            if (!$this->isDevelopmentOrigin($origin)) {
                http_response_code(403);
                echo json_encode(['error' => 'Origin not allowed']);
                exit;
            }
        }
        
        // Set CORS headers
        header('Access-Control-Allow-Origin: ' . $origin);
        header('Access-Control-Allow-Methods: ' . implode(', ', $this->allowed_methods));
        header('Access-Control-Allow-Headers: ' . implode(', ', $this->allowed_headers));
        header('Access-Control-Max-Age: ' . $this->max_age);
        header('Access-Control-Allow-Credentials: true');
        
        // Handle preflight requests
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            http_response_code(204);
            exit;
        }
    }
    
    private function isDevelopmentOrigin($origin) {
        // Allow localhost in development only
        if ($_ENV['APP_ENV'] === 'development') {
            return preg_match('/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/', $origin);
        }
        return false;
    }
}

// Usage
$cors = new CORSHandler();
$cors->handleRequest();
?>
CORS Security: Never use Access-Control-Allow-Origin: * for APIs that require authentication. Always validate origins against a whitelist. Be cautious with Access-Control-Allow-Credentials.

API Versioning Security

API versioning allows you to maintain backward compatibility while evolving your API. However, older API versions can become security liabilities if not properly maintained or deprecated.

<?php
// URL-based versioning with security controls
class APIRouter {
    private $supported_versions = ['v1', 'v2', 'v3'];
    private $deprecated_versions = ['v1'];
    private $minimum_secure_version = 'v2';
    
    public function route($version, $endpoint) {
        // Validate version
        if (!in_array($version, $this->supported_versions, true)) {
            http_response_code(404);
            echo json_encode([
                'error' => 'API version not found',
                'supported_versions' => $this->supported_versions,
            ]);
            exit;
        }
        
        // Warn about deprecated versions
        if (in_array($version, $this->deprecated_versions, true)) {
            header('Deprecation: true');
            header('Sunset: Wed, 31 Dec 2024 23:59:59 GMT');
            header('Link: <https://api.example.com/v3>; rel="successor-version"');
        }
        
        // Enforce minimum version for sensitive endpoints
        if ($this->isSensitiveEndpoint($endpoint)) {
            if (version_compare($version, $this->minimum_secure_version, '<')) {
                http_response_code(426); // Upgrade Required
                echo json_encode([
                    'error' => 'This endpoint requires API version ' . $this->minimum_secure_version . ' or higher',
                ]);
                exit;
            }
        }
        
        // Route to version-specific handler
        $handler_class = "API\" . strtoupper($version) . "\" . $endpoint;
        if (!class_exists($handler_class)) {
            http_response_code(404);
            echo json_encode(['error' => 'Endpoint not found']);
            exit;
        }
        
        $handler = new $handler_class();
        return $handler->handle();
    }
    
    private function isSensitiveEndpoint($endpoint) {
        $sensitive = ['users', 'payments', 'admin'];
        return in_array($endpoint, $sensitive, true);
    }
}
?>
Version Management: Clearly document deprecation timelines, provide migration guides, and send notifications to API consumers. Consider implementing sunset headers to communicate version end-of-life dates.

Input Validation for APIs

APIs require rigorous input validation because they often receive data from untrusted sources and automated clients. Unlike web forms with browser validation, APIs must validate all inputs server-side with strict type checking and format validation.

<?php
class APIValidator {
    public static function validateUserCreate($data) {
        $errors = [];
        
        // Validate email
        if (!isset($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            $errors['email'] = 'Valid email address is required';
        }
        
        // Validate username
        if (!isset($data['username']) || !preg_match('/^[a-zA-Z0-9_]{3,20}$/', $data['username'])) {
            $errors['username'] = 'Username must be 3-20 alphanumeric characters';
        }
        
        // Validate password strength
        if (!isset($data['password']) || strlen($data['password']) < 8) {
            $errors['password'] = 'Password must be at least 8 characters';
        }
        
        // Validate required fields exist
        $required = ['first_name', 'last_name'];
        foreach ($required as $field) {
            if (!isset($data[$field]) || trim($data[$field]) === '') {
                $errors[$field] = ucfirst(str_replace('_', ' ', $field)) . ' is required';
            }
        }
        
        // Validate data types
        if (isset($data['age']) && (!is_int($data['age']) || $data['age'] < 18 || $data['age'] > 120)) {
            $errors['age'] = 'Age must be an integer between 18 and 120';
        }
        
        // Reject unknown fields (prevent mass assignment)
        $allowed_fields = ['email', 'username', 'password', 'first_name', 'last_name', 'age'];
        $unknown_fields = array_diff(array_keys($data), $allowed_fields);
        if (!empty($unknown_fields)) {
            $errors['_meta'] = 'Unknown fields: ' . implode(', ', $unknown_fields);
        }
        
        return $errors;
    }
    
    public static function validateJSON() {
        $input = file_get_contents('php://input');
        $data = json_decode($input, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            http_response_code(400);
            echo json_encode(['error' => 'Invalid JSON: ' . json_last_error_msg()]);
            exit;
        }
        
        return $data;
    }
}

// Usage
$data = APIValidator::validateJSON();
$errors = APIValidator::validateUserCreate($data);

if (!empty($errors)) {
    http_response_code(422);
    echo json_encode(['errors' => $errors]);
    exit;
}
?>
Exercise: Create a secure RESTful API with the following features: JWT authentication, rate limiting (100 requests per hour), CORS configuration for specific origins, input validation for all endpoints, and proper error responses with appropriate HTTP status codes. Implement at least 3 CRUD endpoints.

API Security Monitoring and Logging

Comprehensive logging and monitoring are essential for detecting security incidents, debugging issues, and understanding API usage patterns. Log authentication attempts, rate limit violations, validation errors, and suspicious patterns.

<?php
class APILogger {
    private $log_file;
    
    public function __construct($log_file = '/var/log/api.log') {
        $this->log_file = $log_file;
    }
    
    public function logRequest($user_id, $endpoint, $method, $status_code, $duration_ms) {
        $log_entry = json_encode([
            'timestamp' => date('c'),
            'user_id' => $user_id,
            'ip' => $_SERVER['REMOTE_ADDR'],
            'endpoint' => $endpoint,
            'method' => $method,
            'status_code' => $status_code,
            'duration_ms' => $duration_ms,
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
        ]) . "\n";
        
        file_put_contents($this->log_file, $log_entry, FILE_APPEND | LOCK_EX);
    }
    
    public function logSecurityEvent($event_type, $details) {
        $log_entry = json_encode([
            'timestamp' => date('c'),
            'event_type' => $event_type,
            'ip' => $_SERVER['REMOTE_ADDR'],
            'details' => $details,
        ]) . "\n";
        
        file_put_contents($this->log_file, $log_entry, FILE_APPEND | LOCK_EX);
        
        // Alert for critical events
        if (in_array($event_type, ['brute_force', 'sql_injection', 'privilege_escalation'])) {
            $this->sendSecurityAlert($event_type, $details);
        }
    }
    
    private function sendSecurityAlert($event_type, $details) {
        // Send email/Slack notification for critical security events
        // Implementation depends on your notification system
    }
}
?>
What to Log: Log authentication attempts (success and failure), authorization failures, input validation errors, rate limit violations, unusual access patterns, and all administrative actions. Never log sensitive data like passwords or full credit card numbers.

Best Practices Summary

API security requires multiple layers of defense working together. Always use HTTPS to encrypt data in transit. Implement strong authentication using OAuth 2.0 or JWT with proper token management. Apply rate limiting to prevent abuse and DDoS attacks.

Validate all inputs strictly on the server side, rejecting malformed requests immediately. Configure CORS carefully to allow only trusted origins. Use API versioning to maintain security while evolving your API, and deprecate old versions systematically.

Monitor your API continuously for suspicious activity. Log security events and set up alerts for potential attacks. Implement proper error handling that provides useful information to legitimate users without revealing system internals to attackers.

Defense in Depth: No single security measure is perfect. Layer multiple security controls—authentication, authorization, input validation, rate limiting, and monitoring—to create robust API security.

Keep your dependencies updated and follow security advisories for libraries you use. Conduct regular security audits and penetration testing. Document your API security policies and train your development team on secure coding practices. Remember that API security is an ongoing process, not a one-time implementation.