Complete guide to building secure REST APIs in PHP with JWT authentication, rate limiting, input validation, CORS handling, and production-ready code examples.

PHP Problem Solving: Building Secure REST APIs with Authentication and Rate Limiting

Building secure and scalable REST APIs is a critical skill for modern PHP developers. This comprehensive guide covers authentication mechanisms, request validation, rate limiting, and security best practices to help you create robust API endpoints that protect your data and serve your users efficiently.

FOCUS AREA: This tutorial covers JWT authentication, API request validation, rate limiting implementations, CORS handling, input sanitization, and complete working examples of building production-ready REST APIs in PHP.

Problem 1: Implementing JWT Authentication

Challenge: You need to implement secure authentication for your REST API using JSON Web Tokens (JWT) with proper token generation, validation, and refresh mechanisms.

<?php /** * JWT Authentication Implementation * Problem: Secure API authentication with token management */ class JWTAuthHandler { private string $secretKey; private string $algorithm = 'HS256'; private int $accessTokenExpiry = 3600; // 1 hour private int $refreshTokenExpiry = 604800; // 7 days public function __construct(string $secretKey) { $this->secretKey = $secretKey; } /** * Generate JWT token */ public function generateToken(array $payload, string $type = 'access'): string { $header = json_encode(['typ' => 'JWT', 'alg' => $this->algorithm]); $time = time(); $tokenData = array_merge($payload, [ 'iat' => $time, 'exp' => $time + ($type === 'access' ? $this->accessTokenExpiry : $this->refreshTokenExpiry), 'type' => $type ]); $payload = json_encode($tokenData); $base64Header = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header)); $base64Payload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload)); $signature = hash_hmac('sha256', $base64Header . "." . $base64Payload, $this->secretKey, true); $base64Signature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature)); return $base64Header . "." . $base64Payload . "." . $base64Signature; } /** * Validate and decode JWT token */ public function validateToken(string $token): ?array { $parts = explode('.', $token); if (count($parts) !== 3) { return null; } [$base64Header, $base64Payload, $base64Signature] = $parts; // Verify signature $signature = hash_hmac('sha256', $base64Header . "." . $base64Payload, $this->secretKey, true); $expectedSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature)); if (!hash_equals($expectedSignature, $base64Signature)) { return null; } // Decode payload $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $base64Payload)), true); if (!$payload || !isset($payload['exp'])) { return null; } // Check expiration if ($payload['exp'] < time()) { return null; } return $payload; } /** * Refresh access token using refresh token */ public function refreshAccessToken(string $refreshToken): ?array { $payload = $this->validateToken($refreshToken); if (!$payload || $payload['type'] !== 'refresh') { return null; } // Remove sensitive data from payload unset($payload['iat'], $payload['exp'], $payload['type']); return [ 'access_token' => $this->generateToken($payload, 'access'), 'refresh_token' => $this->generateToken($payload, 'refresh'), 'expires_in' => $this->accessTokenExpiry ]; } /** * Middleware to authenticate requests */ public function authenticate(): ?array { $headers = getallheaders(); $authHeader = $headers['Authorization'] ?? ''; if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { http_response_code(401); echo json_encode(['error' => 'No token provided']); exit; } $token = $matches[1]; $payload = $this->validateToken($token); if (!$payload) { http_response_code(401); echo json_encode(['error' => 'Invalid or expired token']); exit; } return $payload; } } // Usage example $auth = new JWTAuthHandler('your-secret-key-here-min-32-chars'); // Generate tokens after successful login $userPayload = [ 'user_id' => 123, 'email' => 'user@example.com', 'role' => 'user' ]; $tokens = [ 'access_token' => $auth->generateToken($userPayload, 'access'), 'refresh_token' => $auth->generateToken($userPayload, 'refresh'), 'expires_in' => 3600 ]; // Authenticate protected route $userData = $auth->authenticate(); echo "Authenticated user: " . $userData['email']; ?>

Security Note: Never expose your JWT secret key in client-side code or version control. Use environment variables and secure key management practices in production environments.

Problem 2: API Rate Limiting and Request Throttling

Challenge: You need to implement rate limiting to prevent API abuse and ensure fair usage across all clients while maintaining performance.

<?php /** * Rate Limiting Implementation * Problem: Prevent API abuse with request throttling */ class RateLimiter { private $storage; private array $limits; public function __construct($storage = null) { $this->storage = $storage ?? new FileStorage(); $this->limits = [ 'default' => ['requests' => 100, 'window' => 3600], // 100 requests per hour 'authenticated' => ['requests' => 1000, 'window' => 3600], // 1000 per hour 'burst' => ['requests' => 10, 'window' => 60] // 10 per minute for burst control ]; } /** * Check if request is allowed */ public function isAllowed(string $identifier, string $type = 'default'): bool { $limit = $this->limits[$type] ?? $this->limits['default']; $key = "rate_limit:{$type}:{$identifier}"; $current = $this->storage->get($key); $now = time(); if (!$current || $current['reset_at'] < $now) { // New window $current = [ 'count' => 1, 'reset_at' => $now + $limit['window'] ]; $this->storage->set($key, $current, $limit['window']); return true; } if ($current['count'] >= $limit['requests']) { return false; } $current['count']++; $this->storage->set($key, $current, $limit['window']); return true; } /** * Get rate limit headers */ public function getHeaders(string $identifier, string $type = 'default'): array { $limit = $this->limits[$type] ?? $this->limits['default']; $key = "rate_limit:{$type}:{$identifier}"; $current = $this->storage->get($key); $remaining = $limit['requests'] - ($current['count'] ?? 0); $reset = $current['reset_at'] ?? (time() + $limit['window']); return [ 'X-RateLimit-Limit' => $limit['requests'], 'X-RateLimit-Remaining' => max(0, $remaining), 'X-RateLimit-Reset' => $reset ]; } /** * Rate limit middleware */ public function middleware(string $identifier, string $type = 'default'): void { if (!$this->isAllowed($identifier, $type)) { http_response_code(429); $headers = $this->getHeaders($identifier, $type); foreach ($headers as $header => $value) { header("{$header}: {$value}"); } echo json_encode(['error' => 'Rate limit exceeded']); exit; } // Add rate limit headers to successful response $headers = $this->getHeaders($identifier, $type); foreach ($headers as $header => $value) { header("{$header}: {$value}"); } } } /** * Simple file-based storage for rate limiting */ class FileStorage { private string $path; public function __construct(string $path = __DIR__ . '/cache/') { $this->path = $path; if (!is_dir($this->path)) { mkdir($this->path, 0755, true); } } public function get(string $key): ?array { $file = $this->path . md5($key) . '.ratelimit'; if (!file_exists($file)) { return null; } $data = unserialize(file_get_contents($file)); if ($data['expires'] < time()) { unlink($file); return null; } return $data['value']; } public function set(string $key, array $value, int $ttl): void { $file = $this->path . md5($key) . '.ratelimit'; $data = [ 'expires' => time() + $ttl, 'value' => $value ]; file_put_contents($file, serialize($data), LOCK_EX); } } // Usage example $rateLimiter = new RateLimiter(); // Apply rate limiting based on IP address $clientId = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $rateLimiter->middleware($clientId, 'default'); // Or for authenticated users (higher limits) // $rateLimiter->middleware($userId, 'authenticated'); echo json_encode(['message' => 'API response within rate limit']); ?>

Problem 3: Input Validation and Sanitization

Challenge: You need to validate and sanitize all API inputs to prevent injection attacks, ensure data integrity, and provide meaningful error messages.

<?php /** * Input Validation and Sanitization * Problem: Secure API input handling */ class InputValidator { private array $errors = []; private array $validated = []; /** * Validate input data against rules */ public function validate(array $data, array $rules): bool { $this->errors = []; $this->validated = []; foreach ($rules as $field => $ruleSet) { $value = $data[$field] ?? null; $fieldRules = is_array($ruleSet) ? $ruleSet : explode('|', $ruleSet); foreach ($fieldRules as $rule) { $this->applyRule($field, $value, $rule, $data); } } return empty($this->errors); } /** * Apply individual validation rule */ private function applyRule(string $field, $value, string $rule, array $allData): void { // Parse rule with parameters if (strpos($rule, ':') !== false) { [$ruleName, $parameter] = explode(':', $rule, 2); } else { $ruleName = $rule; $parameter = null; } switch ($ruleName) { case 'required': if (empty($value) && $value !== '0' && $value !== 0) { $this->errors[$field][] = "{$field} is required"; } break; case 'email': if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) { $this->errors[$field][] = "{$field} must be a valid email"; } break; case 'min': if (!empty($value) && strlen($value) < (int)$parameter) { $this->errors[$field][] = "{$field} must be at least {$parameter} characters"; } break; case 'max': if (!empty($value) && strlen($value) > (int)$parameter) { $this->errors[$field][] = "{$field} must not exceed {$parameter} characters"; } break; case 'numeric': if (!empty($value) && !is_numeric($value)) { $this->errors[$field][] = "{$field} must be numeric"; } break; case 'integer': if (!empty($value) && !filter_var($value, FILTER_VALIDATE_INT)) { $this->errors[$field][] = "{$field} must be an integer"; } break; case 'in': $allowed = explode(',', $parameter); if (!empty($value) && !in_array($value, $allowed, true)) { $this->errors[$field][] = "{$field} must be one of: " . implode(', ', $allowed); } break; case 'confirmed': $confirmField = $field . '_confirmation'; if ($value !== ($allData[$confirmField] ?? null)) { $this->errors[$field][] = "{$field} confirmation does not match"; } break; case 'unique': // Database uniqueness check (simplified) // In real implementation, query database break; } } /** * Sanitize input data */ public function sanitize(array $data, array $fields): array { $sanitized = []; foreach ($fields as $field => $type) { $value = $data[$field] ?? null; switch ($type) { case 'string': $sanitized[$field] = htmlspecialchars(trim($value ?? ''), ENT_QUOTES, 'UTF-8'); break; case 'email': $sanitized[$field] = filter_var(trim($value ?? ''), FILTER_SANITIZE_EMAIL); break; case 'url': $sanitized[$field] = filter_var(trim($value ?? ''), FILTER_SANITIZE_URL); break; case 'int': $sanitized[$field] = filter_var($value, FILTER_VALIDATE_INT) ?: 0; break; case 'float': $sanitized[$field] = filter_var($value, FILTER_VALIDATE_FLOAT) ?: 0.0; break; case 'bool': $sanitized[$field] = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false; break; case 'array': $sanitized[$field] = is_array($value) ? $value : []; break; default: $sanitized[$field] = $value; } } return $sanitized; } /** * Get validation errors */ public function errors(): array { return $this->errors; } /** * Get validated data */ public function validated(): array { return $this->validated; } } // Usage example $validator = new InputValidator(); $inputData = [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'securepass123', 'password_confirmation' => 'securepass123', 'age' => '25' ]; $rules = [ 'name' => 'required|string|min:2|max:100', 'email' => 'required|email|max:255', 'password' => 'required|string|min:8|confirmed', 'age' => 'required|integer' ]; if ($validator->validate($inputData, $rules)) { $sanitized = $validator->sanitize($inputData, [ 'name' => 'string', 'email' => 'email', 'password' => 'string', 'age' => 'int' ]); echo "Validation passed!"; print_r($sanitized); } else { http_response_code(422); echo json_encode(['errors' => $validator->errors()]); } ?>

Problem 4: Complete API Endpoint Implementation

<?php /** * Complete REST API Endpoint * Putting it all together */ class SecureAPIController { private JWTAuthHandler $auth; private RateLimiter $rateLimiter; private InputValidator $validator; private PDO $db; public function __construct(PDO $db, string $jwtSecret) { $this->db = $db; $this->auth = new JWTAuthHandler($jwtSecret); $this->rateLimiter = new RateLimiter(); $this->validator = new InputValidator(); // Set CORS headers header('Content-Type: application/json'); header('Access-Control-Allow-Origin: https://yourdomain.com'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } } /** * Handle user registration */ public function register(): void { try { // Rate limiting $clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $this->rateLimiter->middleware($clientIp, 'burst'); // Get and validate input $input = json_decode(file_get_contents('php://input'), true); $rules = [ 'email' => 'required|email|max:255', 'password' => 'required|string|min:8|confirmed', 'name' => 'required|string|min:2|max:100' ]; if (!$this->validator->validate($input, $rules)) { http_response_code(422); echo json_encode(['errors' => $this->validator->errors()]); return; } $data = $this->validator->sanitize($input, [ 'email' => 'email', 'password' => 'string', 'name' => 'string' ]); // Check if email exists $stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?"); $stmt->execute([$data['email']]); if ($stmt->fetch()) { http_response_code(409); echo json_encode(['error' => 'Email already registered']); return; } // Create user $hashedPassword = password_hash($data['password'], PASSWORD_ARGON2ID); $stmt = $this->db->prepare("INSERT INTO users (email, password, name) VALUES (?, ?, ?)"); $stmt->execute([$data['email'], $hashedPassword, $data['name']]); $userId = $this->db->lastInsertId(); // Generate tokens $tokens = $this->auth->generateToken([ 'user_id' => $userId, 'email' => $data['email'], 'name' => $data['name'] ]); http_response_code(201); echo json_encode([ 'message' => 'User registered successfully', 'user_id' => $userId, 'tokens' => $tokens ]); } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => 'Internal server error']); error_log($e->getMessage()); } } /** * Get user profile (protected route) */ public function getProfile(): void { try { // Authenticate user $userData = $this->auth->authenticate(); // Rate limiting for authenticated users $this->rateLimiter->middleware((string)$userData['user_id'], 'authenticated'); // Fetch user data $stmt = $this->db->prepare("SELECT id, email, name, created_at FROM users WHERE id = ?"); $stmt->execute([$userData['user_id']]); $user = $stmt->fetch(); if (!$user) { http_response_code(404); echo json_encode(['error' => 'User not found']); return; } echo json_encode([ 'user' => $user, 'authenticated' => true ]); } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => 'Internal server error']); } } } // Route handling $db = new PDO('mysql:host=localhost;dbname=api_db', 'user', 'pass'); $api = new SecureAPIController($db, 'your-secret-key-here'); $path = $_SERVER['REQUEST_URI'] ?? ''; $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; switch ($path) { case '/api/register': if ($method === 'POST') { $api->register(); } break; case '/api/profile': if ($method === 'GET') { $api->getProfile(); } break; default: http_response_code(404); echo json_encode(['error' => 'Endpoint not found']); } ?>

API Security Checklist

Always Use HTTPS: Never transmit tokens or sensitive data over HTTP

Validate All Inputs: Never trust client-provided data

Implement Rate Limiting: Prevent abuse and ensure fair usage

Use Strong Password Hashing: Always use password_hash() with modern algorithms

Set Proper CORS Headers: Control which domains can access your API

Log Security Events: Monitor for suspicious activity

Building Production-Ready APIs

Creating secure and scalable REST APIs requires attention to authentication, validation, rate limiting, and error handling. The patterns shown here provide a solid foundation for building APIs that protect your data while serving your users efficiently.

Remember that security is an ongoing process, not a one-time implementation. Regularly review your API security practices, monitor for vulnerabilities, and stay updated on best practices in API development.

Mastering API Development

The techniques covered in this tutorial—JWT authentication, rate limiting, input validation, and complete endpoint implementation—form the core skills needed for modern PHP API development. By applying these patterns and maintaining security best practices, you can build robust APIs that scale with your application's needs.

Continue practicing these concepts with real-world projects, and always prioritize security and performance in your API design decisions.