
Если вас заинтересовала тема авторизации, подразумеваю, что вы уже итак знаете что такое Telegram Mini Apps. Поэтому не буду долго размусоливать вступление и перейду сразу к делу.
Поехали!
Принцип работы
Так как Telegram Mini Apps — это обычные веб‑приложения, то сценарии аутентификации и авторизации мы будем использовать привычные для веб‑приложений.
Аутентификация
Напомню, это процесс, когда клиент подтверждает, что действительно является тем, за кого себя выдает. В случае с Telegram Mini Apps пользователь аутентифицируется в самом Telegram, и мессенджер самостоятельно передает в наше Mini App данные о пользователе. Однако они могут быть скомпрометированы, и мы не можем им доверять — об этом сказано в документации Telegram Mini Apps.
Нам необходимо провалидировать полученные от Telegram данные и на их основе выдать клиенту токены доступа к нашему серверу, с помощью которых он сможет выполнять свои запросы в дальнейшем.
Хочу отметить, что в качестве механизма аутентификации не обязательно использовать концепцию пары JWT‑токенов — иногда лучшим решением могут оказаться обычные сессии. Всё зависит от архитектуры вашего проекта.

Процесс выглядит следующим образом:
При запуске нашего Mini App Telegram передает в него initData — строку с данными о пользователе и прочей информацией.
Mini App делает запрос на аутентификацию к серверу, передавая ему initData.
Сервер проверяет подлинность initData с помощью bot_token (токена Telegram‑бота, к которому привязан Mini App) посредством сравнения хэшей (далее разберем это подробно).
Из initData извлекается id пользователя в Telegram. По этому id запрашивается пользователь в БД (если он уже есть в системе) или создается новый (если пользователь зашел впервые).
БД возвращает пользователя.
Генерируются access‑ и refresh‑токены. При необходимости в них добавляются данные о пользователе (роли, id и т. д.).
Сервер отвечает на запрос Mini App, устанавливая пару токенов в http‑only secure cookies.
Авторизация
Напомню, это процесс проверки прав пользователя на выполнение им действия в системе. Здесь все как обычно.

Mini App выполняет запрос к серверу (в cookies лежит пара токенов).
Сервер извлекает из cookies пару токенов, проверяет их валидность и расшифровывает.
Если время жизни access-токена истекло, а refresh-токен еще актуален, сервер обновляет токены клиенту.
Сервер авторизует запрос (по ролям, id пользователя или другим атрибутам).
Запрос и получение данных из БД.
Ответ клиенту (если токены обновились — обновляются cookies).
С теорией разобрались, теперь пойдем кодить!
Пример реализации
Этот пример составлен из фрагментов одного из моих проектов и упрощен до предела, чтобы максимально ясно передать суть, не отвлекая на детали.
Frontend реализуем с помощью React, а backend на Nest.js.
Frontend
Сперва нужно подключить Telegram SDK. Для этого нужно добавить скрипт в head страницы:
<script src="https://telegram.org/js/telegram-web-app.js"></script>
Теперь создадим простой React-компонент, который будет отправлять запрос на аутентификацию, передавая initData, и отображать статус сессии:
import axios from 'axios'; import { useState, useEffect } from 'react'; // Кастомный хук для аутентификации const useAuth = () => { // Состояние, указывающее, авторизован ли пользователь const [isAuth, setIsAuth] = useState(false); // Функция для отправки данных на сервер и получения статуса аутентификации const signIn = async (initData: string) => { const { data } = await axios.post<boolean>( 'https://example.com/auth/signin', // URL эндпоинта аутентификации { initData }, // Передаем данные для входа ); setIsAuth(data); // Устанавливаем статус аутентификации }; return { isAuth, signIn }; }; export const App = () => { const { isAuth, signIn } = useAuth(); useEffect(() => { // Вызываем signIn при монтировании компонента, // передавая initData из Telegram WebApp API signIn(window.Telegram.WebApp.initData); }, []); // Если пользователь аутентифицирован, показываем соответствующее сообщение if (isAuth) { return <h1>Authenticated</h1>; } // Если не аутентифицирован, показываем другое сообщение return <h1>Not Authenticated</h1>; };
Backend
Для валидации initData на сервере воспользуемся пакетом @telegram-apps/init-data-node.
JWT-токены будем генерировать с помощью пакета jsonwebtoken.
Сделаем небольшой контроллер, который при обращении будет валидировать initData и выдавать пару токенов:
import { FastifyReply, FastifyRequest } from 'fastify'; import { Controller, HttpStatus, Post, Req, Res, BadRequestException } from '@nestjs/common'; import { parse, isValid } from '@telegram-apps/init-data-node'; import * as jwt from 'jsonwebtoken'; import { getUserByTgId } from './user.service'; @Controller('/auth') export class AuthController { // Эндпоинт аутентификации @Post('/signin') async signin(@Req() req: FastifyRequest, @Res() res: FastifyReply) { const { initData } = req.body as { initData: string }; // Достаем данные из запроса // Валидируем initData с помощью токена бота (он фейковый) const isInitDataValid = isValid( initData, '1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', ); if (!isInitDataValid) { throw new BadRequestException('AUTH__INVALID_INITDATA'); // Ошибка, если initData некорректна } // Парсим initData и достаем Telegram ID пользователя const tgId = parse(initData).user?.id; if (!tgId) { throw new BadRequestException('AUTH__INVALID_INITDATA'); // Ошибка, если ID отсутствует } // Допустим, что тут мы достаем пользователя из базы const user = await getUserByTgId({ tg_id: tgId }); if (!user) { throw new BadRequestException('AUTH__USER_NOT_FOUND'); // Ошибка, если пользователь не найден } const { id, tg_id, roles } = user; // Достаем нужные данные // Создаем access и refresh токены, зашивая в них данные пользователя const accessToken = jwt.sign( { id, tg_id, roles }, 'jwt_at_secret', // Секрет для access-токена { expiresIn: '5m' }, // Время жизни токена ); const refreshToken = jwt.sign( { id, tg_id, roles }, 'jwt_rt_secret', // Секрет для refresh-токена { expiresIn: '7d' }, // Время жизни токена ); // Опции для установки cookies const cookiesOptions = { httpOnly: true, // Доступно только через HTTP (JS не может прочитать) secure: true, // Передается только по HTTPS path: '/', // Доступно во всем домене sameSite: 'strict', // Защита от CSRF-атак }; // Устанавливаем токены в cookies res.cookie('ACCESS_TOKEN', accessToken, cookiesOptions); res.cookie('REFRESH_TOKEN', refreshToken, cookiesOptions); res.status(HttpStatus.OK).send(true); // Отправляем успешный ответ } }
Теперь рассмотрим реализацию авторизации запроса.
Добавим эндпоинт, который:
Возвращает true, если access-токен валиден.
Если access-токен недействителен, пытается обновить его с помощью refresh-токена.
Если обновление не удается, возвращает ошибку 401.
import { FastifyReply, FastifyRequest } from 'fastify'; import { Controller, Get, HttpStatus, Req, Res, UnauthorizedException } from '@nestjs/common'; import * as jwt from 'jsonwebtoken'; @Controller('/auth') export class AuthController { // Эндпоинт для проверки авторизации @Get('/protected') async protected(@Req() req: FastifyRequest, @Res() res: FastifyReply) { const accessToken = req.cookies.ACCESS_TOKEN; // Достаем access-токен из cookies const refreshToken = req.cookies.REFRESH_TOKEN; // Достаем refresh-токен из cookies if (!accessToken || !refreshToken) { throw new UnauthorizedException(); // Ошибка, если нет токенов } try { jwt.verify(accessToken, 'jwt_at_secret'); // Валидируем access-токен return true; // Если токен валиден — отправляем успешный ответ } catch { try { // Валидируем refresh-токен const { id, tg_id, roles } = jwt.verify(refreshToken, 'jwt_rt_secret') as { id: number; tg_id: number; roles: string[]; }; // Создаем новые access и refresh токены const accessToken = jwt.sign( { id, tg_id, roles }, 'jwt_at_secret', // Секрет для access-токена { expiresIn: '5m' }, // Время жизни токена ); const refreshToken = jwt.sign( { id, tg_id, roles }, 'jwt_rt_secret', // Секрет для refresh-токена { expiresIn: '7d' }, // Время жизни токена ); // Опции для установки cookies const cookiesOptions = { httpOnly: true, // Доступно только через HTTP (JS не может прочитать) secure: true, // Передается только по HTTPS path: '/', // Доступно во всем домене sameSite: 'strict', // Защита от CSRF-атак }; // Устанавливаем токены в cookies res.cookie('ACCESS_TOKEN', accessToken, cookiesOptions); res.cookie('REFRESH_TOKEN', refreshToken, cookiesOptions); return true; // Отправляем успешный ответ } catch { throw new UnauthorizedException(); // Ошибка, если refresh-токен недействителен } } } }
Надеюсь, общий принцип вам понятен. Конечно, в реальных приложениях на стороне Frontend потребуется реализовать более сложный хук авторизации, добавить роутинг и защищенные маршруты, а в Nest.js — выносить логику из контроллера в Guards и сервисы. Но это уже тема для других статей.
