Telegram Mini App на Vue и FastAPI: initData и авторизация
Как связать Vue 3 Mini App с FastAPI, передать Telegram initData, проверить HMAC-подпись на сервере и не получить Invalid signature.
Telegram Mini App на Vue и FastAPI: безопасная авторизация
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 проверяет подпись, срок действия и извлекает пользователя.
Подключение 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
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:
values = dict(parse_qsl(init_data, keep_blank_values=True))
received_hash = values.pop("hash", None)
if not 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")
auth_date = int(values.get("auth_date", "0"))
if auth_date <= 0 or time.time() - auth_date > max_age:
raise TelegramAuthError("initData expired")
return json.loads(values["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 открывается через кнопку
web_app, а не обычную ссылку. initDataотправляется без ручного декодирования.- Backend использует токен того же бота.
- Проверяется
auth_date. - ID пользователя не принимается из JSON запроса.
- API работает только через HTTPS.
- Ошибка авторизации показывает понятный экран с повторным открытием Mini App.
Для локальной разработки сделайте отдельный явно включаемый preview-режим. Не ослабляйте production-проверку пустым заголовком или специальным Telegram ID.
Следующий шаг
Обзор возможностей Mini Apps есть в статье что такое Telegram Mini App. Практическая разработка бота, Mini App, платежей и деплоя собрана в полном курсе по Telegram-ботам.
Нужен Mini App под запись, магазин или личный кабинет — опишите задачу.
поделиться
Нужен сайт или веб-приложение?
Разрабатываю веб-сервисы, личные кабинеты и API на современном стеке.