service 4

This commit is contained in:
Hamza-Ayed
2026-05-02 14:50:16 +03:00
parent a6a620f002
commit 40d37cd0d9
2 changed files with 76 additions and 5 deletions

View File

@@ -41,8 +41,16 @@ class JwtService
$this->fpPepper = getenv('FP_PEPPER') ?: ''; $this->fpPepper = getenv('FP_PEPPER') ?: '';
$this->issuer = (string)(getenv('APP_ISSUER') ?: ''); $this->issuer = (string)(getenv('APP_ISSUER') ?: '');
$this->redis = $redis; $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 ────────────────────────────────── // ── توليد Access Token ──────────────────────────────────
public function generateAccessToken( public function generateAccessToken(
int|string $userId, int|string $userId,
@@ -76,9 +84,28 @@ class JwtService
$payload['fingerPrint'] = hash('sha256', $fingerprint . $this->fpPepper); $payload['fingerPrint'] = hash('sha256', $fingerprint . $this->fpPepper);
} }
return JWT::encode($payload, $this->secretKey, self::ALGO); $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 ───────────────────────────────── // ── توليد Refresh Token ─────────────────────────────────
public function generateRefreshToken(int|string $userId): array public function generateRefreshToken(int|string $userId): array
{ {
@@ -176,12 +203,21 @@ class JwtService
if ($this->fpPepper && $tokenType === 'access') { if ($this->fpPepper && $tokenType === 'access') {
$fpInToken = $decoded->fingerPrint ?? null; $fpInToken = $decoded->fingerPrint ?? null;
$fpHeader = $_SERVER['HTTP_X_DEVICE_FP'] ?? 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) { if ($fpInToken === null || $fpHeader === null) {
error_log("[SECURITY] Fingerprint missing | user: $userId");
$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'); self::abort(403, 'Device verification required');
} }
$expected = hash('sha256', $fpHeader . $this->fpPepper); $expected = hash('sha256', $fpHeader . $this->fpPepper);
if (!hash_equals($expected, $fpInToken)) { if (!hash_equals($expected, $fpInToken)) {
error_log("[SECURITY] Device mismatch | user: $userId | IP: " . ($_SERVER['REMOTE_ADDR'] ?? '?')); error_log("[SECURITY] Device mismatch | user: $userId | IP: " . ($_SERVER['REMOTE_ADDR'] ?? '?'));
@@ -232,7 +268,13 @@ class JwtService
} }
} }
public function getFpPepper(): string
{
return $this->fpPepper;
}
private static function abort(int $code, string $message) 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'] ?? '?')); error_log("[JWT_AUTH_FAILED] Code: $code | Message: $message | IP: " . ($_SERVER['REMOTE_ADDR'] ?? '?') . " | URI: " . ($_SERVER['REQUEST_URI'] ?? '?'));
http_response_code($code); http_response_code($code);

View File

@@ -45,17 +45,46 @@ try {
unset($user['password']); unset($user['password']);
// توليد التوكن // توليد التوكن أو استرجاع التوكن الحالي إذا كان صالحاً
$jwtService = new JwtService($redis); $jwtService = new JwtService($redis);
$role = 'service'; $role = 'service';
$jwt = $jwtService->generateAccessToken($user['id'], $role, $audience); $ttl = 14400; // 4 hours
$jwt = null;
$expires_in = $ttl;
// محاولة استعادة التوكن الحالي من Redis لتجنب التكرار
if ($redis) {
$existingToken = $redis->get("active_token:{$user['id']}:{$audience}");
if ($existingToken) {
$decoded = $jwtService->decodeToken($existingToken);
// يجب أن يكون التوكن صالحاً ويحتوي على بصمة الجهاز (إذا كان التشفير مفعلاً)
if ($decoded && $decoded->exp > time() && (isset($decoded->fingerPrint) || empty($jwtService->getFpPepper()))) {
$jwt = $existingToken;
$expires_in = $decoded->exp - time();
}
}
}
// إذا لم يوجد توكن صالح، نولد واحداً جديداً ونلغي القديم
if (!$jwt) {
if ($redis) {
$oldJti = $redis->get("active_jti:{$user['id']}");
if ($oldJti) {
$jwtService->revokeToken($oldJti, $ttl);
}
}
$jwt = $jwtService->generateAccessToken($user['id'], $role, $audience, $fingerprint);
$expires_in = $ttl;
}
printSuccess([ printSuccess([
"message" => "Login successful", "message" => "Login successful",
"data" => $user, "data" => $user,
"jwt" => $jwt, "jwt" => $jwt,
"expires_in" => 3600 "expires_in" => $expires_in
]); ]);
} else { } else {
jsonError("Incorrect password"); jsonError("Incorrect password");
} }