
Если вас заинтересовала тема авторизации, подразумеваю, что вы уже итак знаете что такое 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 и сервисы. Но это уже тема для других статей.