154 lines
5.2 KiB
PHP
154 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Filters;
|
|
|
|
use App\Libraries\RateLimiterService;
|
|
use CodeIgniter\Filters\FilterInterface;
|
|
use CodeIgniter\HTTP\RequestInterface;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
/**
|
|
* JwtApiFilter
|
|
*
|
|
* Applied to API routes that require a valid JWT token.
|
|
* Identity (email or mobile) is extracted from the JWT payload using
|
|
* your existing helper functions: getEmailFromJWT() / getMobileFromJWT().
|
|
*
|
|
* Performs:
|
|
* - IP-level throttle + progressive block check (via fingerprint)
|
|
* - User-level throttle + progressive block check (by JWT identity)
|
|
*
|
|
* Usage in Routes.php:
|
|
* $routes->get('api/profile', 'ProfileController::index', ['filter' => 'jwtApiRateLimit']);
|
|
*
|
|
* Register in app/Config/Filters.php:
|
|
* 'JwtApiRateLimitFilter' => \App\Filters\JwtApiRateLimitFilter::class
|
|
*/
|
|
class JwtApiRateLimitFilter implements FilterInterface
|
|
{
|
|
protected RateLimiterService $limiter;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->limiter = new RateLimiterService();
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// BEFORE — runs before the controller
|
|
// -------------------------------------------------------------------------
|
|
|
|
public function before(RequestInterface $request, $arguments = null)
|
|
{
|
|
$fingerprint = generateFingerprint(exclude_ua: true);
|
|
|
|
|
|
// 1. IP-level throttle + block check
|
|
$ipResult = $this->limiter->checkIp($fingerprint, 'jwtApi');
|
|
if ($ipResult) {
|
|
return $this->jsonResponse($ipResult);
|
|
}
|
|
|
|
// 2. Resolve user identity from JWT
|
|
// Uses your existing JWT helper functions.
|
|
// If neither returns a value, fall back to IP-only limiting.
|
|
$identity = $this->resolveIdentityFromJwt();
|
|
|
|
if ($identity) {
|
|
// User-level throttle (request count based for JWT routes)
|
|
$userResult = $this->limiter->checkUserThrottle($identity, 'jwtApi');
|
|
if ($userResult) {
|
|
return $this->jsonResponse($userResult);
|
|
}
|
|
}
|
|
|
|
// Stash for after() use
|
|
$request->setGlobal('rateLimitFingerprint', $fingerprint);
|
|
if ($identity) {
|
|
$request->setGlobal('rateLimitIdentity', $identity);
|
|
}
|
|
|
|
return null; // pass through
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// AFTER — records IP failure on controller-level bad responses
|
|
// -------------------------------------------------------------------------
|
|
|
|
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
|
{
|
|
$statusCode = $response->getStatusCode();
|
|
|
|
// Only act on auth-related failures from the controller (401, 422, etc.)
|
|
// 429/403/451 are already handled by before(); skip 2xx/3xx.
|
|
if ($statusCode < 400 || in_array($statusCode, [429, 403, 451])) {
|
|
return;
|
|
}
|
|
|
|
$fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint(exclude_ua: true);
|
|
|
|
$identity = $request->getGlobal('rateLimitIdentity') ?? $this->resolveIdentityFromJwt();
|
|
|
|
$this->limiter->recordIpFailure($fingerprint);
|
|
|
|
if ($identity) {
|
|
$this->limiter->recordUserFailure($identity, 'jwtApi');
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// HELPERS
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Resolve user identity from JWT using your existing helper functions.
|
|
* Tries email first, then mobile. Returns null if JWT is absent/invalid.
|
|
*
|
|
* IMPORTANT: Replace getEmailFromJWT() / getMobileFromJWT() with your
|
|
* actual function names if they differ.
|
|
*/
|
|
protected function resolveIdentityFromJwt(): ?string
|
|
{
|
|
try {
|
|
// Try email from JWT
|
|
if (function_exists('getEmailFromJWT')) {
|
|
$email = getEmailFromJWT();
|
|
if ($email) {
|
|
return strtolower(trim($email));
|
|
}
|
|
}
|
|
|
|
// Try mobile from JWT
|
|
if (function_exists('getMobileFromJWT')) {
|
|
$mobile = getMobileFromJWT();
|
|
if ($mobile) {
|
|
return trim($mobile);
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// JWT invalid or expired — fall through to IP-only limiting
|
|
log_message('debug', '[RateLimiter] JWT identity resolution failed: ' . $e->getMessage());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Build and return a JSON response for blocked/throttled requests.
|
|
*/
|
|
protected function jsonResponse(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',
|
|
],
|
|
]));
|
|
return $response;
|
|
}
|
|
}
|