108 lines
3.8 KiB
PHP
108 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
use App\Services\PayloadCrypto;
|
|
|
|
/**
|
|
* وسيط التحقق من التوقيع الرقمي (HMAC Validation Middleware)
|
|
*
|
|
* الغرض من الملف:
|
|
* توفير طبقة أمان إضافية لجميع الطلبات لضمان عدم التلاعب بالبيانات أثناء انتقالها بين التطبيق والخادم.
|
|
*
|
|
* كيفية العمل:
|
|
* 1. يتطلب وجود "توقيع" (Signature) في رأس الطلب (Headers).
|
|
* 2. يقوم الخادم بإعادة حساب التوقيع باستخدام مفتاح سري (API Secret) ومقارنته بالتوقيع المرسل.
|
|
* 3. حماية البيانات: إذا كانت البيانات مشفرة، يقوم بفك تشفيرها باستخدام نفس المفتاح السري (API Secret).
|
|
* 4. يضمن أن البيانات لم تتغير في الطريق (Data Integrity).
|
|
*/
|
|
class HmacAuthMiddleware
|
|
{
|
|
private const ALGORITHM = 'sha256';
|
|
private PayloadCrypto $crypto;
|
|
|
|
public function __construct(PayloadCrypto $crypto)
|
|
{
|
|
$this->crypto = $crypto;
|
|
}
|
|
|
|
public function handle(Request $request, Closure $next)
|
|
{
|
|
$signature = $request->header('X-Signature');
|
|
$timestamp = $request->header('X-Timestamp');
|
|
$nonce = $request->header('X-Nonce');
|
|
$apiKey = $request->header('X-API-Key');
|
|
|
|
// All headers required
|
|
if (!$signature || !$timestamp || !$nonce || !$apiKey) {
|
|
return response()->json([
|
|
'status' => 'failure',
|
|
'message' => 'Missing security headers'
|
|
], 401);
|
|
}
|
|
|
|
// Reject if timestamp older than tolerance (replay protection)
|
|
$tolerance = config('intaleq.hmac_tolerance', 300);
|
|
if (abs(time() - (int) $timestamp) > $tolerance) {
|
|
return response()->json([
|
|
'status' => 'failure',
|
|
'message' => 'Request expired'
|
|
], 401);
|
|
}
|
|
|
|
// Nonce replay check (prevent duplicate requests)
|
|
$nonceKey = 'nonce:' . $nonce;
|
|
if (Cache::has($nonceKey)) {
|
|
return response()->json([
|
|
'status' => 'failure',
|
|
'message' => 'Duplicate request'
|
|
], 401);
|
|
}
|
|
Cache::put($nonceKey, true, $tolerance);
|
|
|
|
// Lookup api_secret by api_key
|
|
$user = DB::connection('primary')->table('passengers')
|
|
->where('api_key', $apiKey)->first(['api_secret']);
|
|
if (!$user) {
|
|
$user = DB::connection('primary')->table('driver')
|
|
->where('api_key', $apiKey)->first(['api_secret']);
|
|
}
|
|
|
|
if (!$user) {
|
|
return response()->json([
|
|
'status' => 'failure',
|
|
'message' => 'Invalid API key'
|
|
], 401);
|
|
}
|
|
|
|
// Compute expected signature
|
|
$method = strtoupper($request->method());
|
|
$path = $request->path(); // returns path without leading slash (e.g. v2/ride/create)
|
|
|
|
if (str_contains(strtolower($request->header('Content-Type', '')), 'multipart/form-data')) {
|
|
$inputs = $request->except(array_keys($request->allFiles()));
|
|
ksort($inputs);
|
|
$body = json_encode($inputs);
|
|
} else {
|
|
$body = $request->getContent();
|
|
}
|
|
|
|
$payload = "$method:$path:$timestamp:$nonce:$body";
|
|
$expected = hash_hmac(self::ALGORITHM, $payload, $user->api_secret);
|
|
|
|
if (!hash_equals($expected, $signature)) {
|
|
return response()->json([
|
|
'status' => 'failure',
|
|
'message' => 'Invalid signature'
|
|
], 401);
|
|
}
|
|
|
|
return $next($request);
|
|
}
|
|
}
|