initData: проверка подлинности пользователя на бэкенде

Проверишь подпись initData через HMAC-SHA256 и безопасно получишь user id на сервере.

Зачем валидировать initData

Telegram.WebApp.initData — строка параметров (как query string), которую Telegram подписывает. На клиенте её легко подделать. Источник истины — проверка HMAC на бэкенде с токеном бота: только после этого можно доверять user.id и выдавать сессию.

Без проверки любой может отправить на API { user: { id: 1 } } и выдать себя за другого.


Проверяем подпись на Node.js

Алгоритм из официальной документации:

  1. Из initData достаёшь hash, остальные пары сортируешь и склеиваешь через \n.
  2. secret_key = HMAC_SHA256(bot_token, key = "WebAppData").
  3. Сравниваешь hex(HMAC_SHA256(data_check_string, secret_key)) с hash.
  4. Дополнительно отбрасываешь устаревшие данные по auth_date.
typescript
// Node.js 20+, встроенный crypto (Bot API WebApps validating data)
import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_AGE_SEC = 60 * 60; // 1 час — подстрой под продукт

export type TelegramWebAppUser = {
  id: number;
  first_name: string;
  last_name?: string;
  username?: string;
  language_code?: string;
  is_premium?: boolean;
};

export function validateInitData(
  initData: string,
  botToken: string,
  maxAgeSec = MAX_AGE_SEC,
): { user: TelegramWebAppUser; authDate: number } {
  const params = new URLSearchParams(initData);
  const hash = params.get("hash");
  if (!hash) {
    throw new Error("initData: отсутствует hash");
  }

  params.delete("hash");

  const dataCheckString = [...params.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([key, value]) => `${key}=${value}`)
    .join("\n");

  const secretKey = createHmac("sha256", "WebAppData")
    .update(botToken)
    .digest();

  const calculated = createHmac("sha256", secretKey)
    .update(dataCheckString)
    .digest("hex");

  const calculatedBuf = Buffer.from(calculated, "hex");
  const hashBuf = Buffer.from(hash, "hex");
  if (
    calculatedBuf.length !== hashBuf.length ||
    !timingSafeEqual(calculatedBuf, hashBuf)
  ) {
    throw new Error("initData: неверная подпись");
  }

  const authDate = Number(params.get("auth_date"));
  if (!Number.isFinite(authDate)) {
    throw new Error("initData: нет auth_date");
  }
  const now = Math.floor(Date.now() / 1000);
  if (now - authDate > maxAgeSec) {
    throw new Error("initData: данные устарели");
  }

  const userRaw = params.get("user");
  if (!userRaw) {
    throw new Error("initData: нет user");
  }

  const user = JSON.parse(userRaw) as TelegramWebAppUser;
  if (!user?.id) {
    throw new Error("initData: некорректный user");
  }

  return { user, authDate };
}

// Пример Express-роута
import express from "express"; // express ^4.21

const app = express();
app.use(express.json());

app.post("/api/auth", (req, res) => {
  try {
    const { initData } = req.body as { initData?: string };
    if (!initData) {
      res.status(400).json({ error: "initData required" });
      return;
    }

    const { user } = validateInitData(initData, process.env.BOT_TOKEN!);
    // Здесь создаёшь JWT / session cookie, привязанную к user.id
    res.json({ ok: true, telegramId: user.id, name: user.first_name });
  } catch {
    res.status(401).json({ error: "unauthorized" });
  }
});

На клиенте после ready():

typescript
const initData = window.Telegram.WebApp.initData;

await fetch("https://api.example.com/api/auth", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ initData }),
});

Как это работает

  • hash — HMAC от всех полей кроме самого hash.
  • secret_key — HMAC от токена бота с ключом "WebAppData" (не путать с Login Widget, там другой алгоритм).
  • auth_date — unix-время выдачи; без TTL replay возможен.
  • user — JSON-строка внутри query; парсишь только после успешной проверки hash.

Частые ошибки

Валидируешь только на клиенте или читаешь initDataUnsafe → для продакшна недостаточно. Сессию выдаёт только бэкенд после HMAC.

Путаешь алгоритм с Telegram Login Widget → у Login Widget другой secret (SHA256(bot_token)). Для Mini App — HMAC с ключом "WebAppData".

Не проверяешь auth_date → украденный старый initData можно переиспользовать. Ставь разумный TTL.


Что дальше

Материалы gramdocs.tech носят образовательный характер и не являются финансовой, юридической или инвестиционной рекомендацией. Работа с блокчейном TON и токеном Gram связана с рисками потери средств. Правовая информация