Проверка initData в Telegram Mini App на Python: код и ошибки
initData из Mini App нужно проверять на сервере: секрет вычисляется как HMAC-SHA256 токена бота с ключом WebAppData, затем сравнивается подпись строки данных. Обязательны сравнение через compare_digest и проверка auth_date, чтобы отсечь подделку и старые данные.
Когда Mini App обращается к вашему серверу, вам нужно убедиться, что запрос действительно пришёл из Telegram от конкретного пользователя, а не от злоумышленника, который подставил чужой user_id. Для этого Telegram подписывает строку initData, а сервер проверяет подпись.
Как устроена проверка
Mini App получает Telegram.WebApp.initData — строку вида query_id=...&user=...&auth_date=...&hash=.... Проверка делается так:
- Разобрать строку на пары ключ-значение (значения декодируются из URL).
- Убрать поле
hash, остальные пары отсортировать по ключу и склеить в строкуключ=значение, разделяя переводом строки. - Вычислить секретный ключ: HMAC-SHA256, где ключ — строка
WebAppData, а сообщение — токен бота. - Вычислить HMAC-SHA256 строки из шага 2 с этим секретом и сравнить результат (в hex) с полученным
hash. - Проверить, что
auth_dateне старше разумного срока.
Рабочий код
import hashlib
import hmac
import json
import time
from urllib.parse import parse_qsl
def validate_init_data(init_data: str, bot_token: str, max_age: int = 3600) -> dict:
"""Возвращает данные Mini App, если подпись верна и данные свежие, иначе бросает ValueError."""
pairs = dict(parse_qsl(init_data, keep_blank_values=True))
received_hash = pairs.pop("hash", None)
if not received_hash:
raise ValueError("нет hash")
check_string = "\n".join(f"{k}={v}" for k, v in sorted(pairs.items()))
secret_key = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
expected = hmac.new(secret_key, check_string.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, received_hash):
raise ValueError("подпись не совпала")
if time.time() - int(pairs.get("auth_date", 0)) > max_age:
raise ValueError("данные устарели")
if "user" in pairs:
pairs["user"] = json.loads(pairs["user"])
return pairs
Мы прогнали этот код на подписанном образце, на подмене данных, на чужом токене и на устаревшем auth_date: корректные данные принимаются, остальные отклоняются.
Как передать initData на сервер
Фронтенд отправляет строку в заголовке запроса, а сервер проверяет её на каждом обращении:
fetch("/api/me", { headers: { Authorization: "tma " + Telegram.WebApp.initData } });
Префикс tma — распространённая договорённость, а не требование Telegram. Пример проверки в aiohttp:
from aiohttp import web
async def me(request: web.Request) -> web.Response:
auth = request.headers.get("Authorization", "")
if not auth.startswith("tma "):
raise web.HTTPUnauthorized()
try:
data = validate_init_data(auth[4:], BOT_TOKEN)
except ValueError:
raise web.HTTPUnauthorized()
return web.json_response({"user_id": data["user"]["id"]})
В FastAPI та же функция подключается как зависимость (Depends), которая читает заголовок и вызывает validate_init_data.
Типичные ошибки
- Проверка на клиенте. Подпись должна проверяться только на сервере: токен бота нельзя отдавать во фронтенд.
- Сравнение через
==. Используйтеhmac.compare_digest, чтобы не открыть атаку по времени ответа. - Забытый
auth_date. Без проверки срока старые перехваченные данные работают вечно. - Неправильный порядок ключей или лишние поля в строке проверки. Поле
hashв строку не входит, остальные идут отсортированными по алфавиту. - Не тот токен. Подпись привязана к токену конкретного бота. Если у вас несколько ботов, храните токен вместе с приложением.
- Доверие к
initDataUnsafe. Это те же данные без проверки. Использовать их для авторизации нельзя.
Где это пригодится
Проверка initData нужна любому Mini App, который хранит данные пользователя: избранное, подписки на уведомления, сохранённые поиски. Так устроены Mini App в наших проектах: Da Nang House и HotTrip. Если вы строите бота или Mini App, посмотрите услугу «Telegram-боты и Mini App».
Частые вопросы
Можно ли доверять user_id из initDataUnsafe?
Нет. `initDataUnsafe` — это распарсенные данные без проверки подписи. Для авторизации используйте только данные, которые прошли `validate_init_data`.
Как долго initData действителен?
Telegram передаёт время создания в `auth_date`, а срок годности определяете вы. Для обычных запросов хватает часа, для чувствительных операций берут меньше.
Что делать при ошибке «подпись не совпала»?
Проверьте, что токен принадлежит тому боту, из которого открыт Mini App, что `hash` исключён из строки проверки, что пары отсортированы по ключу и склеены переводом строки.
Нужна ли проверка, если Mini App только читает публичные данные?
Если данные публичны и не привязаны к пользователю, проверка не обязательна. Как только появляются личные данные или действия от имени пользователя, она обязательна.
Версия для ИИ и чтения без оформления: Markdown.