Если вас заинтересовала тема авторизации, подразумеваю, что вы уже итак знаете что такое Telegram Mini Apps. Поэтому не буду долго размусоливать вступление и перейду сразу к делу.

Поехали!

Принцип работы

Так как Telegram Mini Apps — это обычные веб‑приложения, то сценарии аутентификации и авторизации мы будем использовать привычные для веб‑приложений.

Аутентификация

Напомню, это процесс, когда клиент подтверждает, что действительно является тем, за кого себя выдает. В случае с Telegram Mini Apps пользователь аутентифицируется в самом Telegram, и мессенджер самостоятельно передает в наше Mini App данные о пользователе. Однако они могут быть скомпрометированы, и мы не можем им доверять — об этом сказано в документации Telegram Mini Apps.

Нам необходимо провалидировать полученные от Telegram данные и на их основе выдать клиенту токены доступа к нашему серверу, с помощью которых он сможет выполнять свои запросы в дальнейшем.

Хочу отметить, что в качестве механизма аутентификации не обязательно использовать концепцию пары JWT‑токенов — иногда лучшим решением могут оказаться обычные сессии. Всё зависит от архитектуры вашего проекта.

Аутентификация
Аутентификация

Процесс выглядит следующим образом:

  1. При запуске нашего Mini App Telegram передает в него initData — строку с данными о пользователе и прочей информацией.

  2. Mini App делает запрос на аутентификацию к серверу, передавая ему initData.

  3. Сервер проверяет подлинность initData с помощью bot_token (токена Telegram‑бота, к которому привязан Mini App) посредством сравнения хэшей (далее разберем это подробно).

  4. Из initData извлекается id пользователя в Telegram. По этому id запрашивается пользователь в БД (если он уже есть в системе) или создается новый (если пользователь зашел впервые).

  5. БД возвращает пользователя.

  6. Генерируются access‑ и refresh‑токены. При необходимости в них добавляются данные о пользователе (роли, id и т. д.).

  7. Сервер отвечает на запрос Mini App, устанавливая пару токенов в http‑only secure cookies.

Авторизация

Напомню, это процесс проверки прав пользователя на выполнение им действия в системе. Здесь все как обычно.

Авторизация
Авторизация
  1. Mini App выполняет запрос к серверу (в cookies лежит пара токенов).

  2. Сервер извлекает из cookies пару токенов, проверяет их валидность и расшифровывает.

  3. Если время жизни access-токена истекло, а refresh-токен еще актуален, сервер обновляет токены клиенту.

  4. Сервер авторизует запрос (по ролям, id пользователя или другим атрибутам).

  5. Запрос и получение данных из БД.

  6. Ответ клиенту (если токены обновились — обновляются 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 и сервисы. Но это уже тема для других статей.