все статьи
24 августа 2026 г.·13 мин

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

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

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

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