service 4
This commit is contained in:
@@ -41,8 +41,16 @@ class JwtService
|
||||
$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,
|
||||
@@ -76,9 +84,28 @@ class JwtService
|
||||
$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 ─────────────────────────────────
|
||||
public function generateRefreshToken(int|string $userId): array
|
||||
{
|
||||
@@ -176,12 +203,21 @@ class JwtService
|
||||
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) {
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
$expected = hash('sha256', $fpHeader . $this->fpPepper);
|
||||
if (!hash_equals($expected, $fpInToken)) {
|
||||
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)
|
||||
|
||||
{
|
||||
error_log("[JWT_AUTH_FAILED] Code: $code | Message: $message | IP: " . ($_SERVER['REMOTE_ADDR'] ?? '?') . " | URI: " . ($_SERVER['REQUEST_URI'] ?? '?'));
|
||||
http_response_code($code);
|
||||
|
||||
@@ -45,17 +45,46 @@ try {
|
||||
|
||||
unset($user['password']);
|
||||
|
||||
// توليد التوكن
|
||||
// توليد التوكن أو استرجاع التوكن الحالي إذا كان صالحاً
|
||||
$jwtService = new JwtService($redis);
|
||||
$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([
|
||||
"message" => "Login successful",
|
||||
"data" => $user,
|
||||
"jwt" => $jwt,
|
||||
"expires_in" => 3600
|
||||
"expires_in" => $expires_in
|
||||
]);
|
||||
|
||||
} else {
|
||||
jsonError("Incorrect password");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user