initData в Telegram Mini App: авторизация React + Vite — KEL IT
Telegram Mini Apps 7 мин чтения

initData в Telegram Mini App: авторизация React + Vite

Telegram Mini App открывается внутри клиента Telegram и сразу передаёт данные о пользователе через initData. Это удобно: не нужна отдельная регистрация, OAuth или SMS. Но есть критичный нюанс — клиент нельзя доверять. Любой может подделать запрос с чужим user.id, если бэкенд не проверяет подпись.

В этой статье разберём production-подход: React + Vite на фронте, Node.js на бэкенде, проверка initData по алгоритму Telegram и выдача JWT-сессии. Без «магии SDK» — с пониманием, что происходит под капотом.

Почему initData — единственный надёжный источник identity

Когда пользователь открывает Mini App, Telegram передаёт строку вида:

query_id=AAHdF...&user=%7B%22id%22%3A279058397...&auth_date=1700000000&hash=abc123...

Поля внутри:

  • user — JSON с id, first_name, username, language_code
  • auth_date — Unix-время создания данных
  • hash — HMAC-SHA256 подпись, которую может проверить только сервер с bot token

Если вы читаете window.Telegram.WebApp.initDataUnsafe.user на клиенте и сразу пускаете пользователя в личный кабинет — это не авторизация, а декорация. Злоумышленник откроет DevTools, подставит чужой id и получит доступ к чужим данным.

Правило простое: клиент отправляет сырую строку initData, сервер проверяет подпись и только потом создаёт сессию.

Структура проекта

mini-app/
├── frontend/          # React + Vite
│   ├── src/
│   │   ├── main.tsx
│   │   ├── App.tsx
│   │   └── api/auth.ts
│   └── package.json
└── backend/           # Node.js + Fastify
    ├── src/
    │   ├── index.ts
    │   ├── validateInitData.ts
    │   └── routes/auth.ts
    └── package.json

Стек в 2026 году:

  • @telegram-apps/sdk-react — официальный SDK для React (заменил устаревший @twa-dev/sdk)
  • Fastify — лёгкий HTTP-сервер
  • jose — подпись и проверка JWT без лишних зависимостей

Если вам нужна подобная разработка под ключ — напишите в Telegram, разберём задачу и предложим решение.

Фронтенд: получаем initData и отправляем на сервер

Установка зависимостей:

npm create vite@latest frontend -- --template react-ts
cd frontend
npm install @telegram-apps/sdk-react

Инициализация SDK в main.tsx:

import { init } from '@telegram-apps/sdk-react';

init(); // Подключает Telegram WebApp API

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Компонент авторизации:

// src/App.tsx
import { useEffect, useState } from 'react';
import { useLaunchParams, useRawInitData } from '@telegram-apps/sdk-react';
import { loginWithInitData } from './api/auth';

export default function App() {
  const initData = useRawInitData();
  const launchParams = useLaunchParams();
  const [user, setUser] = useState<{ id: number; firstName: string } | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!initData) return;

    loginWithInitData(initData)
      .then(setUser)
      .catch((err) => setError(err.message));
  }, [initData]);

  if (error) return <p>Ошибка входа: {error}</p>;
  if (!user) return <p>Проверяем данные Telegram…</p>;

  return (
    <div>
      <h1>Привет, {user.firstName}!</h1>
      <p>Telegram ID: {user.id}</p>
      <p>Platform: {launchParams.platform}</p>
    </div>
  );
}

API-клиент:

// src/api/auth.ts
const API_URL = import.meta.env.VITE_API_URL;

export async function loginWithInitData(initData: string) {
  const res = await fetch(`${API_URL}/auth/telegram`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ initData }),
  });

  if (!res.ok) {
    const { message } = await res.json();
    throw new Error(message ?? 'Auth failed');
  }

  const { token, user } = await res.json();
  localStorage.setItem('access_token', token);
  return user;
}

Обратите внимание: на сервер уходит полная строка initData, а не распарсенный объект user.

Бэкенд: алгоритм проверки подписи

Telegram описывает алгоритм в документации Web Apps. Реализация на Node.js:

// backend/src/validateInitData.ts
import crypto from 'node:crypto';

interface TelegramUser {
  id: number;
  first_name: string;
  username?: string;
  language_code?: string;
}

export function validateInitData(
  initData: string,
  botToken: string,
  maxAgeSeconds = 86400 // 24 часа
): TelegramUser {
  const params = new URLSearchParams(initData);
  const hash = params.get('hash');
  if (!hash) throw new Error('Missing hash');

  params.delete('hash');

  // Сортируем ключи и собираем data-check-string
  const dataCheckString = [...params.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([key, value]) => `${key}=${value}`)
    .join('\n');

  // secret_key = HMAC-SHA256("WebAppData", bot_token)
  const secretKey = crypto
    .createHmac('sha256', 'WebAppData')
    .update(botToken)
    .digest();

  const calculatedHash = crypto
    .createHmac('sha256', secretKey)
    .update(dataCheckString)
    .digest('hex');

  if (calculatedHash !== hash) {
    throw new Error('Invalid initData signature');
  }

  const authDate = Number(params.get('auth_date'));
  const now = Math.floor(Date.now() / 1000);
  if (now - authDate > maxAgeSeconds) {
    throw new Error('initData expired');
  }

  const userRaw = params.get('user');
  if (!userRaw) throw new Error('Missing user field');

  return JSON.parse(userRaw) as TelegramUser;
}

Ключевые моменты:

  1. Параметр hash исключается из строки перед вычислением HMAC
  2. Пары key=value сортируются по алфавиту
  3. Разделитель — символ перевода строки \n, не &
  4. Проверяйте auth_date, иначе перехваченную строку можно использовать бесконечно

Выдача JWT-сессии

После успешной проверки создайте или найдите пользователя в БД и верните токен:

// backend/src/routes/auth.ts
import { FastifyPluginAsync } from 'fastify';
import { SignJWT } from 'jose';
import { validateInitData } from '../validateInitData';

const authRoutes: FastifyPluginAsync = async (app) => {
  app.post('/auth/telegram', async (request, reply) => {
    const { initData } = request.body as { initData?: string };

    if (!initData) {
      return reply.status(400).send({ message: 'initData required' });
    }

    let tgUser;
    try {
      tgUser = validateInitData(initData, process.env.BOT_TOKEN!);
    } catch (err) {
      return reply.status(401).send({
        message: err instanceof Error ? err.message : 'Unauthorized',
      });
    }

    // upsert пользователя в PostgreSQL / MongoDB
    const user = await app.db.upsertUser({
      telegramId: tgUser.id,
      firstName: tgUser.first_name,
      username: tgUser.username,
    });

    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const token = await new SignJWT({ sub: String(user.id), tgId: tgUser.id })
      .setProtectedHeader({ alg: 'HS256' })
      .setExpirationTime('7d')
      .sign(secret);

    return { token, user: { id: user.id, firstName: user.firstName } };
  });
};

export default authRoutes;

Дальнейшие запросы фронтенда идут с заголовком Authorization: Bearer <token>. initData нужен только один раз — при входе.

Локальная разработка без Telegram

В браузере initData пустой. Два рабочих подхода:

  1. Telegram Test Environment — BotFather → /newapp → тестовый бот, открытие через t.me
  2. Mock-эндпоинт — только для NODE_ENV=development, генерирует валидный initData с тестовым bot token

Не отключайте проверку подписи в staging — это самая частая дыра в безопасности Mini Apps.

Типичные ошибки и как их избежать

Использование initDataUnsafe для авторизации

initDataUnsafe — распарсенный объект без проверки. Подходит для UI (имя в шапке, аватар), но никогда для доступа к данным.

Неверная сборка data-check-string

Частая ошибка — не сортировать ключи или использовать & вместо \n. Подпись не совпадёт, и вы будете часами искать проблему в bot token.

Отсутствие проверки auth_date

Строка initData перехватывается один раз и переиспользуется. Без проверки времени атакующий получает вечный доступ.

Хранение bot token на фронте

Bot token нужен только на сервере. Если он попадёт в Vite bundle — любой пользователь сможет подписывать произвольные initData.

CORS и mixed content

Mini App работает по HTTPS. Бэкенд тоже должен быть на HTTPS. Для локальной разработки используйте ngrok или Cloudflare Tunnel.

Интеграция с MainButton и темой

После авторизации можно подключить нативные элементы Telegram:

import { mainButton, backButton } from '@telegram-apps/sdk-react';

useEffect(() => {
  if (!user) return;

  mainButton.setParams({
    text: 'Оформить заказ',
    isVisible: true,
    isEnabled: true,
  });

  const offClick = mainButton.onClick(() => submitOrder());

  backButton.show();
  const offBack = backButton.onClick(() => window.history.back());

  return () => {
    offClick();
    offBack();
    mainButton.hide();
    backButton.hide();
  };
}, [user]);

SDK автоматически подстраивает цвета под тему Telegram (themeParams). Не хардкодите #ffffff — используйте CSS-переменные SDK или var(--tg-theme-bg-color).

Заключение

Безопасная авторизация в Telegram Mini App строится на трёх шагах: фронт отправляет сырую строку initData, бэкенд проверяет HMAC-SHA256 подпись с bot token, после чего выдаёт собственную сессию (JWT). Это стандартный паттерн для SaaS, маркетплейсов, сервисов записи и fintech-приложений внутри Telegram.

React + Vite + @telegram-apps/sdk-react закрывают фронтенд-часть за пару часов. Основное время уходит на бэкенд, модель пользователя и обработку edge cases — истёкший auth_date, повторный вход, привязка к существующему аккаунту.

Нужна помощь с реализацией? Я занимаюсь этим профессионально. Написать в Telegram → или vic.kell@ya.ru

FAQ

Можно ли авторизовать пользователя только на фронте без бэкенда?
Нет, если приложение работает с персональными данными или платежами. Без серверной проверки подписи любой может подделать user.id. Для демо-страниц без бэкенда допустимо, для production — нет.

Как часто нужно повторно отправлять initData?
Один раз при входе. После проверки выдаёте JWT или session cookie с TTL 7–30 дней. При истечении сессии пользователь заново открывает Mini App — Telegram передаст свежий initData.

initData отличается от данных Login Widget?
Да. Login Widget использует другой алгоритм (SHA256(bot_token) как secret). Для Mini Apps всегда применяйте WebApp-алгоритм с "WebAppData" в HMAC.

Что делать, если подпись не совпадает?
Проверьте: правильный bot token (от того бота, через которого открыт Mini App), сортировку ключей, разделитель \n, URL-decoding параметров. Часто проблема в том, что фронт отправляет уже декодированную строку повторно.

Нужен ли отдельный бэкенд, если есть aiogram-бот?
Не обязательно. В aiogram 3 можно добавить aiohttp/FastAPI роут /auth/telegram в том же процессе. Главное — не проверять initData внутри хэндлера сообщений, а вынести в отдельный HTTP-эндпоинт для Mini App.

KEL IT

Нужна разработка под ключ?

Я занимаюсь такими проектами профессионально. Telegram-боты, Mini Apps, сайты, мобильные и десктопные приложения. Расскажите о задаче — разберём и предложим решение.