API Security
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.
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.
// 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);
}
?>
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.
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));
?>
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.
// 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);
}
?>
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.
// 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);
}
}
}
?>
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.
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();
?>
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.
// 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);
}
}
?>
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.
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;
}
?>
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.
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
}
}
?>
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.
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.