Files
intaleq_v3_pure_php/core/Auth/JwtService.php
Hamza-Ayed 3fa9aee14e 6
2026-04-28 14:14:11 +03:00

240 lines
9.0 KiB
PHP

<?php
// ============================================================
// core/Auth/JwtService.php
// JWT آمن: JTI + Blacklist في Redis + Refresh Token
// ============================================================
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
class JwtService
{
private string $secretKey;
private string $hmacSecret;
private string $fpPepper;
private ?Redis $redis;
private string $issuer;
private const ACCESS_TTL = 3600; // 1 ساعة
private const REFRESH_TTL = 2592000; // 30 يوم
private const ALGO = 'HS256';
// Endpoints مسموح لها بتوكن نوع registration
private const REGISTRATION_ENDPOINTS = [
'loginFirstTime', 'loginFirstTimeDriver',
'checkPhoneNumberISVerfiedDriver', 'checkPhoneNumberISVerfiedPassenger',
'otpmessage', 'signup', 'verifyEmail', 'verifyOtpMessage',
'sendVerifyEmail', 'sendWhatsAppDriver', 'register_passenger',
'sendWhatsOpt', 'verifyOtp', 'auth_proxy', 'addToken',
'loginFromGoogle', 'loginUsingCredentialsWithoutGoogle',
'register', 'sendOtpMessageDriver', 'getTokensPassenger',
'send_otp', 'verify_otp', 'errorApp', 'register_driver',
];
public function __construct(?Redis $redis = null)
{
$this->secretKey = trim(file_get_contents('/home/intaleq-api/.secret_key'));
$this->hmacSecret = getenv('SECRET_KEY_HMAC') ?: '';
$this->fpPepper = getenv('FP_PEPPER') ?: '';
$this->issuer = getenv('APP_ISSUER') ;
$this->redis = $redis;
}
// ── توليد 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;
}
$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);
}
return JWT::encode($payload, $this->secretKey, self::ALGO);
}
// ── توليد 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
if (($decoded->iss ?? '') !== $this->issuer) {
self::abort(401, 'Invalid token issuer');
}
// 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;
if ($fpInToken === null || $fpHeader === null) {
error_log("[SECURITY] Fingerprint missing | user: $userId");
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;
}
}
private static function abort(int $code, string $message): never
{
http_response_code($code);
echo json_encode(['error' => $message]);
exit;
}
}