service 4
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user