usersFile = $usersFile ?? __DIR__ . '/../config/users.json'; // Load JWT secret from environment variable $this->jwtSecret = $_ENV['JWT_SECRET'] ?? getenv('JWT_SECRET'); if (empty($this->jwtSecret)) { throw new Exception('JWT_SECRET environment variable is not configured'); } } public function login(string $username, string $password): array { $users = $this->loadUsers(); $user = null; foreach ($users['users'] as $candidate) { if (hash_equals($candidate['username'], $username)) { $user = $candidate; break; } } // Always run a password check to avoid a timing oracle on user existence if ($user === null) { password_verify($password, '$2y$10$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG'); return ['success' => false, 'error' => 'Invalid credentials']; } if (!password_verify($password, $user['password'])) { return ['success' => false, 'error' => 'Invalid credentials']; } $token = $this->generateToken($user); return [ 'success' => true, 'token' => $token, 'user' => [ 'username' => $user['username'], 'role' => $user['role'] ] ]; } public function verifyToken(string $token): ?array { $parts = explode('.', $token); if (count($parts) !== 3) { return null; } list($header, $payload, $signature) = $parts; // Verify algorithm header before trusting anything $headerData = json_decode(self::base64UrlDecode($header), true); if (!is_array($headerData) || ($headerData['alg'] ?? '') !== self::JWT_ALGO) { return null; } // Verify signature (constant-time) $expectedSignature = self::base64UrlEncode( hash_hmac('sha256', "$header.$payload", $this->jwtSecret, true) ); if (!hash_equals($expectedSignature, $signature)) { return null; } // Decode payload $payloadData = json_decode(self::base64UrlDecode($payload), true); if (!is_array($payloadData)) { return null; } // Expiry is mandatory if (!isset($payloadData['exp']) || !is_numeric($payloadData['exp']) || (int) $payloadData['exp'] < time()) { return null; } return $payloadData; } private static function base64UrlEncode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } private static function base64UrlDecode(string $data): string { $remainder = strlen($data) % 4; if ($remainder !== 0) { $data .= str_repeat('=', 4 - $remainder); } return base64_decode(strtr($data, '-_', '+/')); } private function generateToken(array $user): string { $header = self::base64UrlEncode(json_encode([ 'alg' => self::JWT_ALGO, 'typ' => 'JWT' ])); $payload = self::base64UrlEncode(json_encode([ 'username' => $user['username'], 'role' => $user['role'], 'iat' => time(), 'exp' => time() + self::JWT_EXPIRY ])); $signature = self::base64UrlEncode( hash_hmac('sha256', "$header.$payload", $this->jwtSecret, true) ); return "$header.$payload.$signature"; } private function loadUsers(): array { if (!file_exists($this->usersFile)) { return ['users' => []]; } $content = file_get_contents($this->usersFile); return json_decode($content, true) ?? ['users' => []]; } }