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_codeauth_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;
}
Ключевые моменты:
- Параметр
hashисключается из строки перед вычислением HMAC - Пары
key=valueсортируются по алфавиту - Разделитель — символ перевода строки
\n, не& - Проверяйте
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 пустой. Два рабочих подхода:
- Telegram Test Environment — BotFather →
/newapp→ тестовый бот, открытие через t.me - 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.