post('api/auth/verify-otp', 'AuthController::verifyOtp', ['filter' => 'authApiRateLimit']); * * Register in app/Config/Filters.php: * 'AuthApiRateLimitFilter' => \App\Filters\AuthApiRateLimitFilter::class */ class AuthApiRateLimitFilter implements FilterInterface { protected RateLimiterService $limiter; protected Cors $corsFilter; public function __construct() { $this->limiter = new RateLimiterService(); $this->corsFilter = new Cors(); } // ------------------------------------------------------------------------- // BEFORE — runs before the controller // ------------------------------------------------------------------------- public function before(RequestInterface $request, $arguments = null) { $fingerprint = generateFingerprint(exclude_ua: true); // 1. IP-level check $ipResult = $this->limiter->checkIp($fingerprint, 'authApi'); if ($ipResult) { return $this->jsonResponse($request, $ipResult); } // 2. User-level block check (identity may not be present yet on first hit) $identity = $this->resolveIdentity($request); if ($identity) { $userResult = $this->limiter->checkUser($identity); if ($userResult) { return $this->jsonResponse($request, $userResult); } } // Store resolved identity in request for use in after() if ($identity) { $request->setGlobal('rateLimitIdentity', $identity); } $request->setGlobal('rateLimitFingerprint', $fingerprint); return null; // pass through } // ------------------------------------------------------------------------- // AFTER — runs after the controller; records failures on bad responses // ------------------------------------------------------------------------- public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { // Only act on failed responses (4xx from auth failures) $statusCode = $response->getStatusCode(); if ($statusCode < 400 || $statusCode === 429 || $statusCode === 403 || $statusCode === 451) { return; // 2xx/3xx = success; 429/403/451 already handled } $fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint(exclude_ua: true); $identity = $request->getGlobal('rateLimitIdentity') ?? $this->resolveIdentity($request); // Record failure at IP level $this->limiter->recordIpFailure($fingerprint); // Record failure at user level if ($identity) { $this->limiter->recordUserFailure($identity, 'authApi'); } } // ------------------------------------------------------------------------- // HELPERS // ------------------------------------------------------------------------- /** * Extract identity from POST body or GET params. * Looks for 'email' or 'mobile_number'. */ protected function resolveIdentity(RequestInterface $request): ?string { // Try POST body first $request_data = $request->getPost() ?: $request->getJSON(true) ?? []; // print_r($request_data); die; $email = $request_data['email'] ?? $request_data['email_id'] ?? null; $mobile = $request_data['mobile_number'] ?? null; // Fallback to GET params if (! $email && ! $mobile) { $email = $request->getGet('email') ?? $request->getGet('email_id'); $mobile = $request->getGet('mobile_number'); } if ($email) { return strtolower(trim($email)); } if ($mobile) { return trim($mobile); } return null; } /** * Build and return a JSON response for blocked/throttled requests. */ protected function jsonResponse(RequestInterface $request, array $result): ResponseInterface { $response = service('response'); $response->setStatusCode($result['status']); $response->setContentType('application/json'); $response->setBody(json_encode([ 'success' => false, 'error' => [ 'code' => strtoupper('RATE_LIMIT_' . $result['level']), 'message' => $result['message'], 'type' => $result['type'] ?? 'request', ], ])); $this->corsFilter->after($request, $response); return $response; } }