secretKey = trim(file_get_contents('/home/intaleq-api/.secret_key')); $this->hmacSecret = getenv('SECRET_KEY_HMAC') ?: ''; $this->fpPepper = getenv('FP_PEPPER') ?: ''; $this->issuer = (string)(getenv('APP_ISSUER') ?: ''); $this->redis = $redis; // Debugging fpPepper if (empty($this->fpPepper)) { error_log("[JWT_DEBUG] fpPepper is EMPTY in constructor"); } else { error_log("[JWT_DEBUG] fpPepper is SET (length: " . strlen($this->fpPepper) . ")"); } } // ── توليد Access Token ────────────────────────────────── public function generateAccessToken( int|string $userId, string $role, string $audience, ?string $fingerprint = null ): string { $jti = bin2hex(random_bytes(16)); $ttl = 3600; if ($role === 'driver') { $ttl = 14400; } elseif ($role === 'passenger') { $ttl = 3600; } elseif ($role === 'service') { $ttl = 14400; // 4 hours as requested } $payload = [ 'iss' => $this->issuer, 'aud' => $audience, 'user_id' => $userId, 'role' => $role, 'token_type' => 'access', 'jti' => $jti, 'iat' => time(), 'exp' => time() + $ttl, ]; if ($fingerprint && $this->fpPepper) { $payload['fingerPrint'] = hash('sha256', $fingerprint . $this->fpPepper); } $token = JWT::encode($payload, $this->secretKey, self::ALGO); // تخزين في Redis لضمان عدم التكرار وإمكانية الإلغاء if ($this->redis) { $this->redis->setex("active_jti:{$userId}", $ttl, $jti); $this->redis->setex("active_token:{$userId}:{$audience}", $ttl, $token); } return $token; } // ── فك تشفير التوكن للتحقق الداخلي ──────────────────────── public function decodeToken(string $token): ?object { try { return JWT::decode($token, new Key($this->secretKey, self::ALGO)); } catch (Exception $e) { return null; } } // ── توليد Refresh Token ───────────────────────────────── public function generateRefreshToken(int|string $userId): array { $token = bin2hex(random_bytes(32)); $exp = time() + self::REFRESH_TTL; // تخزين في Redis if ($this->redis) { $this->redis->setex( "refresh:{$userId}:{$token}", self::REFRESH_TTL, json_encode(['user_id' => $userId, 'created_at' => time()]) ); } return ['token' => $token, 'expires_at' => $exp]; } // ── التحقق الكامل من التوكن ──────────────────────────── public function authenticate(): object { // 1. استخراج التوكن $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; $token = null; if (preg_match('/Bearer\s(\S+)/', $authHeader, $m)) { $token = $m[1]; } if (!$token) { self::abort(401, 'Authorization token required'); } // 2. Decode try { $decoded = JWT::decode($token, new Key($this->secretKey, self::ALGO)); } catch (ExpiredException $e) { self::abort(401, 'Token expired'); } catch (SignatureInvalidException $e) { // محاولة فك التشفير بمفتاح المحفظة (Wallet secret fallback) $payKeyPath = '/home/intaleq-api/.secret_key_pay'; $payKey = file_exists($payKeyPath) ? trim(file_get_contents($payKeyPath)) : ''; if ($payKey) { try { $decoded = JWT::decode($token, new Key($payKey, self::ALGO)); } catch (Exception $e2) { self::abort(401, 'Invalid token signature'); } } else { self::abort(401, 'Invalid token signature'); } } catch (BeforeValidException $e) { self::abort(401, 'Token not yet valid'); } catch (Exception $e) { self::abort(401, 'Invalid token'); } // 3. Issuer (Only check if configured) if (!empty($this->issuer) && ($decoded->iss ?? '') !== $this->issuer) { self::abort(401, 'Invalid token issuer: expected ' . $this->issuer . ' but got ' . ($decoded->iss ?? 'none')); } // 3.1 App Signature Verification (Security Layer) $appSignature = $_SERVER['HTTP_X_APP_SIGNATURE'] ?? null; if ($appSignature === null && function_exists('getallheaders')) { $headers = array_change_key_case(getallheaders(), CASE_LOWER); $appSignature = $headers['x-app-signature'] ?? null; } // قائمة البصمات المعتمدة لكل تطبيق (يجب تعبئتها من ملف .env) // APP_SIGNATURE_SERVICE, APP_SIGNATURE_DRIVER, APP_SIGNATURE_PASSENGER $role = $decoded->role ?? 'unknown'; $envKey = 'APP_SIGNATURE_' . strtoupper($role); $expectedSignature = getenv($envKey) ?: getenv('APP_SIGNATURE_HASH'); if (!empty($expectedSignature)) { if ($appSignature === null || !hash_equals($expectedSignature, $appSignature)) { error_log("[SECURITY_ERROR] App Signature Mismatch/Missing! Role: $role | Expected: $expectedSignature | Got: " . ($appSignature ?? 'NONE') . " | User: $userId"); // الحظر النهائي: إذا كانت البصمة خاطئة، نرفض الطلب فوراً self::abort(403, 'App integrity check failed. Please update your app.'); } } else { // في حال لم يتم ضبط البصمة لهذا النوع من المستخدمين بعد، نسجلها فقط لتسهيل الإعداد error_log("[SECURITY_INFO] Incoming App Signature for $role: " . ($appSignature ?? 'NONE') . " | User: $userId"); } // 4. User ID $userId = $decoded->user_id ?? $decoded->sub ?? null; if (!$userId) { self::abort(401, 'Invalid JWT payload'); } // 5. JTI Blacklist (تحقق من توكنات ملغاة) $jti = $decoded->jti ?? null; if ($jti && $this->redis) { if ($this->redis->exists("jwt:blacklist:$jti")) { self::abort(401, 'Token has been revoked'); } } // 6. token_type — قيّد registration endpoints $tokenType = $decoded->token_type ?? 'access'; if ($tokenType === 'registration' || $tokenType === 'new') { $currentFile = basename($_SERVER['PHP_SELF'], '.php'); $allowed = false; foreach (self::REGISTRATION_ENDPOINTS as $ep) { if (strcasecmp($currentFile, $ep) === 0) { $allowed = true; break; } } if (!$allowed) { error_log("[SECURITY] Registration token blocked on: $currentFile | user: $userId"); self::abort(403, 'Token not authorized for this action'); } } // 7. Device Fingerprint (إلزامي للـ Access Tokens) if ($this->fpPepper && $tokenType === 'access') { $fpInToken = $decoded->fingerPrint ?? null; $fpHeader = $_SERVER['HTTP_X_DEVICE_FP'] ?? null; // محاولة جلب الهيدر بطرق بديلة إذا لم يوجد في $_SERVER if ($fpHeader === null && function_exists('getallheaders')) { $headers = array_change_key_case(getallheaders(), CASE_LOWER); $fpHeader = $headers['x-device-fp'] ?? null; } if ($fpInToken === null || $fpHeader === null) { $allHeaders = json_encode(getallheaders()); error_log("[SECURITY] Fingerprint missing | user: $userId | fpInToken: " . ($fpInToken ?? 'NULL') . " | fpHeader: " . ($fpHeader ?? 'NULL') . " | Headers: $allHeaders"); self::abort(403, 'Device verification required'); } $expected = hash('sha256', $fpHeader . $this->fpPepper); if (!hash_equals($expected, $fpInToken)) { error_log("[SECURITY] Device mismatch | user: $userId | IP: " . ($_SERVER['REMOTE_ADDR'] ?? '?')); self::abort(403, 'Device mismatch'); } } // 8. HMAC — مطلوب للعمليات الحساسة (Wallet/Logout) $hmacHeader = $_SERVER['HTTP_X_HMAC_AUTH'] ?? null; if ($hmacHeader !== null) { $timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? ''; $nonce = $_SERVER['HTTP_X_NONCE'] ?? ''; $body = file_get_contents('php://input') ?: ''; // التوقيع يضم الـ Body + Timestamp + Nonce لمنع التكرار والتلاعب $payloadToSign = $body . $timestamp . $nonce; $expectedHmac = hash_hmac('sha256', $payloadToSign, $this->hmacSecret); if (!hash_equals($expectedHmac, $hmacHeader)) { error_log("[SECURITY] HMAC mismatch | user: $userId | IP: " . ($_SERVER['REMOTE_ADDR'] ?? '?')); self::abort(403, 'Invalid HMAC signature'); } } return $decoded; } // ── إلغاء توكن (Logout / Password Change) ────────────── public function revokeToken(string $jti, int $remainingTTL = 900): void { if ($this->redis && $jti) { $this->redis->setex("jwt:blacklist:$jti", $remainingTTL + 60, '1'); } } // ── Internal API Key — للـ get_connect.php ───────────── public static function validateInternalKey(): void { $keyPath = getenv('INTERNAL_SOCKET_KEY_PATH'); $sent = $_SERVER['HTTP_X_INTERNAL_KEY'] ?? ''; $expected = (file_exists($keyPath) ? trim(file_get_contents($keyPath)) : '') ?: 'Intaleq_Secure_Bridge_Key_2026_@!socket'; if (!$expected || !hash_equals($expected, $sent)) { error_log('[SECURITY] Invalid internal key from: ' . ($_SERVER['REMOTE_ADDR'] ?? '?')); http_response_code(403); echo json_encode(['error' => 'Unauthorized internal request']); exit; } } public function getFpPepper(): string { return $this->fpPepper; } private static function abort(int $code, string $message) { error_log("[JWT_AUTH_FAILED] Code: $code | Message: $message | IP: " . ($_SERVER['REMOTE_ADDR'] ?? '?') . " | URI: " . ($_SERVER['REQUEST_URI'] ?? '?')); http_response_code($code); echo json_encode(['error' => $message]); exit; } }