nhance-enrollment/app/Filters/AuthApiRateLimitFilter.php

152 lines
5.1 KiB
PHP

<?php
namespace App\Filters;
use App\Libraries\RateLimiterService;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use App\Filters\Cors;
/**
* AuthApiFilter
*
* Applied to API routes that do NOT use JWT — e.g. verifyMobileNumber, verifyOTP.
* Identity is extracted from request params: 'email' or 'mobile_number'.
*
* Performs:
* - IP-level throttle + progressive block check (via fingerprint)
* - User-level block check (if identity present in params)
*
* Usage in Routes.php:
* $routes->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;
}
}