все статьи
24 августа 2026 г.·обновлено 12 октября 2026 г.·13 мин

Telegram Mini App на Vue и FastAPI: initData и авторизация

Как связать Vue 3 Mini App с FastAPI, передать Telegram initData, проверить HMAC-подпись на сервере и не получить Invalid signature.

Telegram Mini AppVue.jsFastAPIPythonБезопасность

Автор: Shcoder, независимый разработчик

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 на современном стеке.