Telegram Mini App на Vue и FastAPI: initData и авторизация
Как связать Vue 3 Mini App с FastAPI, передать Telegram initData, проверить HMAC-подпись на сервере и не получить Invalid signature.
Mini App получает Telegram-пользователя без отдельной формы входа. Но данные из браузера нельзя считать доверенными: initDataUnsafe легко подменить вручную. Telegram рекомендует отправлять на backend исходную строку Telegram.WebApp.initData и проверять её подпись на сервере. Алгоритм описан в официальной документации Mini Apps.
Разберём рабочую архитектуру на Vue 3 и FastAPI, включая ошибку Invalid signature, которая часто появляется из-за лишнего декодирования строки.
Архитектура
Поток запроса выглядит так:
Telegram → Vue Mini App → X-Init-Data → FastAPI → проверка HMAC → пользователь
Vue не решает, кто авторизован. Он только прикладывает исходную строку к каждому API-запросу. FastAPI проверяет подпись, срок действия и извлекает пользователя.
Важно: initData зависит от способа запуска. Для авторизации используйте запуск, который передаёт данные пользователя, например Mini App из inline-кнопки бота (InlineKeyboardButton.web_app), меню или Main Mini App. Reply-кнопка KeyboardButton.web_app рассчитана на интерфейс ввода и может не передать initData; для неё предусмотрен отдельный поток sendData(). Проверьте реальное значение Telegram.WebApp.initData в каждом поддерживаемом клиенте до интеграции с API. Список способов запуска и доступных данных.
Подключение Telegram SDK
Добавьте официальный SDK до основного JavaScript-бандла:
<script src="https://telegram.org/js/telegram-web-app.js"></script>
После этого доступен объект window.Telegram.WebApp.
const tg = window.Telegram?.WebApp
tg?.ready()
tg?.expand()
ready() сообщает Telegram, что интерфейс можно показать. Его стоит вызывать после применения темы и подготовки первого экрана, чтобы пользователь не видел пустое состояние.
Передача initData из Vue
Удобнее централизовать заголовок в API-клиенте:
const API_URL = 'https://api.example.com/api/v1'
async function apiRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
const initData = window.Telegram?.WebApp?.initData ?? ''
const response = await fetch(`${API_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Init-Data': initData,
...options.headers,
},
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}
Не отправляйте отдельно user_id, username или факт оплаты. Backend должен брать Telegram ID только из проверенного initData.
Проверка подписи в Python
Строка initData имеет формат query string. Нужно убрать hash, отсортировать остальные пары и собрать их через перенос строки.
import hashlib
import hmac
import json
import time
import re
from urllib.parse import parse_qsl
class TelegramAuthError(ValueError):
pass
def validate_init_data(init_data: str, bot_token: str, max_age: int = 3600) -> dict:
if not init_data or len(init_data) > 16_384:
raise TelegramAuthError("Missing or oversized initData")
try:
pairs = parse_qsl(init_data, keep_blank_values=True, strict_parsing=True)
except ValueError as exc:
raise TelegramAuthError("Malformed initData") from exc
if len({key for key, _ in pairs}) != len(pairs):
raise TelegramAuthError("Duplicate initData field")
values = dict(pairs)
received_hash = values.pop("hash", None)
if not received_hash or not re.fullmatch(r"[0-9a-f]{64}", received_hash):
raise TelegramAuthError("Missing hash")
data_check_string = "\n".join(
f"{key}={value}" for key, value in sorted(values.items())
)
secret_key = hmac.new(
b"WebAppData",
bot_token.encode(),
hashlib.sha256,
).digest()
calculated_hash = hmac.new(
secret_key,
data_check_string.encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(calculated_hash, received_hash):
raise TelegramAuthError("Invalid signature")
try:
auth_date = int(values["auth_date"])
user = json.loads(values["user"])
except (KeyError, TypeError, ValueError) as exc:
raise TelegramAuthError("Malformed auth_date or user") from exc
now = int(time.time())
if auth_date <= 0 or auth_date < now - max_age or auth_date > now + 30:
raise TelegramAuthError("initData expired")
if not isinstance(user, dict) or type(user.get("id")) is not int:
raise TelegramAuthError("Missing Telegram user ID")
return user
hmac.compare_digest нужен для безопасного сравнения. Проверка auth_date ограничивает повторное использование старой строки.
Почему возникает Invalid signature
Самая частая причина — изменить строку до проверки:
# Так делать не нужно
init_data = unquote(init_data)
parse_qsl уже корректно разбирает percent-encoding. Дополнительный unquote() может повторно декодировать символы внутри JSON и изменить data_check_string. В итоге пользователь настоящий, но подпись больше не совпадает.
Другие причины:
- проверка токеном другого бота;
- исключение поля из
data_check_string; - сортировка после изменения значений;
- использование
initDataUnsafeвместо исходной строки; - повторная кодировка пробелов и символа
+; - слишком маленький допустимый возраст
auth_date.
Dependency для FastAPI
Проверку удобно оформить как dependency:
from fastapi import Header, HTTPException
async def telegram_user(x_init_data: str = Header(alias="X-Init-Data")) -> dict:
try:
return validate_init_data(x_init_data, settings.bot_token)
except TelegramAuthError as exc:
raise HTTPException(status_code=401, detail=str(exc)) from exc
Использование в endpoint:
from fastapi import APIRouter, Depends
router = APIRouter()
@router.get("/profile")
async def profile(user: dict = Depends(telegram_user)):
return await profiles.get_or_create(telegram_id=user["id"])
Так один и тот же механизм защищает профиль, курсы, заказы и платежи.
Что проверить перед релизом
- Mini App открывается способом, который передаёт пользовательский
initData(например, inline-кнопкаInlineKeyboardButton.web_app, menu button или Main Mini App). - В реальном Telegram-клиенте проверено, что
Telegram.WebApp.initDataне пустой;KeyboardButton.web_appможет использовать отдельный потокsendData(). initDataотправляется без ручного декодирования.- Backend использует токен того же бота.
- Проверяется
auth_date. - ID пользователя не принимается из JSON запроса.
- API работает только через HTTPS.
- Ошибка авторизации показывает понятный экран с повторным открытием Mini App.
Для локальной разработки сделайте отдельный явно включаемый preview-режим. Не ослабляйте production-проверку пустым заголовком или специальным Telegram ID.
Следующий шаг
Обзор возможностей Mini Apps есть в статье что такое Telegram Mini App. Практическая разработка бота, Mini App, платежей и деплоя собрана в полном курсе по Telegram-ботам.
Нужен Mini App под запись, магазин или личный кабинет — опишите задачу.
по теме
следующий шаг
Собрать production-бота целиком
В полном курсе есть PostgreSQL, платежи, Mini App, тесты и Docker. Начать можно с бесплатного первого урока.
поделиться
Нужен сайт или веб-приложение?
Разрабатываю веб-сервисы, личные кабинеты и API на современном стеке.