Как стать автором
Обновить

Делаем авторизацию в Telegram Mini Apps правильно

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров6.3K

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

Теги:
Хабы:
Всего голосов 8: ↑8 и ↓0+8
Комментарии4

Публикации

Истории

Работа

Ближайшие события

19 марта – 28 апреля
Экспедиция «Рэйдикс»
Нижний НовгородЕкатеринбургНовосибирскВладивостокИжевскКазаньТюменьУфаИркутскЧелябинскСамараХабаровскКрасноярскОмск
22 апреля
VK Видео Meetup 2025
МоскваОнлайн
23 апреля
Meetup DevOps 43Tech
Санкт-ПетербургОнлайн
24 апреля
VK Go Meetup 2025
Санкт-ПетербургОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область