crypto = $crypto; } public function handle(Request $request, Closure $next) { $apiKey = $request->header('X-API-Key'); $timestamp = $request->header('X-Timestamp'); $signature = $request->header('X-Signature'); $nonce = $request->header('X-Nonce'); // 1. Check required headers if (!$apiKey || !$timestamp || !$signature) { return response()->json([ 'status' => 'failure', 'message' => 'Missing authentication headers' ], 401); } // 2. Validate timestamp (prevent replay attacks) $tolerance = (int) config('intaleq.hmac_tolerance', 300); $timeDiff = abs(time() - (int) $timestamp); if ($timeDiff > $tolerance) { return response()->json([ 'status' => 'failure', 'message' => 'Request expired' ], 401); } // 3. Check nonce uniqueness if ($nonce) { $nonceKey = "nonce:{$nonce}"; if (Cache::has($nonceKey)) { return response()->json(['status' => 'failure', 'message' => 'Duplicate request'], 401); } Cache::put($nonceKey, true, $tolerance * 2); } // 4. Lookup API secret from database $credentials = $this->getApiCredentials($apiKey); if (!$credentials) { return response()->json(['status' => 'failure', 'message' => 'Invalid API key'], 401); } // 5. Reconstruct and verify HMAC signature $payload = $request->getContent(); $message = "{$timestamp}|{$apiKey}|{$payload}"; $expectedSignature = hash_hmac(self::ALGORITHM, $message, $credentials->api_secret); if (!hash_equals($expectedSignature, $signature)) { return response()->json(['status' => 'failure', 'message' => 'Invalid signature'], 401); } // 6. Optional: Auto-Decrypt Payload if it's encrypted // We assume if it's a non-JSON string, it might be encrypted if (!empty($payload) && !str_starts_with(trim($payload), '{')) { try { $this->crypto->setKeyFromSecret($credentials->api_secret); $decrypted = $this->crypto->decrypt($payload); if ($decrypted) { // Replace request content with decrypted data $request->initialize( $request->query->all(), $request->request->all(), $request->attributes->all(), $request->cookies->all(), $request->files->all(), $request->server->all(), $decrypted ); // Also merge decrypted JSON into request data $jsonData = json_decode($decrypted, true); if (is_array($jsonData)) { $request->merge($jsonData); } } } catch (\Exception $e) { // If decryption fails, we might still want to proceed if it wasn't meant to be encrypted // but usually, a failed signature check (above) would have caught tampering. } } // 7. Attach user info to request for controllers $request->merge([ '_auth_user_id' => $credentials->user_id, '_auth_user_type' => $credentials->user_type, ]); return $next($request); } /** * Get API credentials with Redis caching (5 min) */ private function getApiCredentials(string $apiKey): ?object { $cacheKey = "api_cred:{$apiKey}"; return Cache::remember($cacheKey, 300, function () use ($apiKey) { // Check both driver and passenger tables for API keys $driver = DB::connection('primary') ->table('driver') ->select('id as user_id', 'api_secret') ->selectRaw("'driver' as user_type") ->where('api_key', $apiKey) ->where('status', 'notDeleted') ->first(); if ($driver) return $driver; $passenger = DB::connection('primary') ->table('passengers') ->select('id as user_id', 'api_secret') ->selectRaw("'passenger' as user_type") ->where('api_key', $apiKey) ->where('status', 'notDeleted') ->first(); if ($passenger) return $passenger; // Check admin users $admin = DB::connection('primary') ->table('adminUser') ->select('id as user_id', 'api_secret') ->selectRaw("'admin' as user_type") ->where('api_key', $apiKey) ->first(); return $admin; }); } }