Законный вход в Telegram через РФ сервисы на примере проекта: «Моя АнтиСоцсеть»

от автора

Если вы когда-нибудь настраивали авторизацию в Telegram Mini Apps, то знаете: всё просто, пока не приходится задумываться о российских требованиях к идентификации. Telegram передаёт initData — строку с подписью HMAC-SHA-256, сервер проверяет её по алгоритму, использующему секрет, производный от bot token. Способ надёжный, проверенный, работает из коробки. Но если ваш сервис рассчитан на российскую аудиторию, одной проверки initData уже недостаточно.

Мы оставили Telegram как удобную точку входа и канал доставки, а идентификацию вынесли в отдельные ID-сервисы. Идея простая: разделить «кто пользователь» (identity) и «куда ему слать уведомления» (channel identity). Пользователь авторизуется через VK ID, Яндекс ID или MAX, а telegram_id позже привязывается к его аккаунту исключительно для отправки ленты — не для входа.

В базе данных это выглядит примерно так:

Поле

Назначение

vk_user_id

идентификатор от VK ID

yandex_id

идентификатор от Яндекс ID

max_user_id

идентификатор от MAX

telegram_id

канал доставки (не для входа)

max_chat_id

канал доставки в MAX

vk_chat_id

канал доставки в VK

В интерфейсе мини-приложения — три кнопки: MAX, VK, Яндекс. Кнопки «Войти через Telegram» нет.

Три кнопки входа: MAX, VK, Яндекс.

Три кнопки входа: MAX, VK, Яндекс.

Для VK ID в нашем flow используется PKCE. Браузер генерирует code_verifier, вычисляет code_challenge (S256) и передаёт его вместе с state в запрос на авторизацию. Сервер сохраняет code_verifier по state.

async function startVkAuth() {    const verifier = generateCodeVerifier();    const challenge = await generateCodeChallenge(verifier);    const state = crypto.randomUUID();    await apiRequest('/api/auth/vk/save-verifier', {        method: 'POST',        body: JSON.stringify({ state, code_verifier: verifier })    });    const authUrl =        `https://id.vk.ru/authorize?client_id=${VK_APP_ID}` +        `&redirect_uri=${encodeURIComponent(redirectUri)}` +        `&state=${encodeURIComponent(state)}` +        `&code_challenge=${encodeURIComponent(challenge)}` +        `&code_challenge_method=S256`;    window.open(authUrl, '_blank');    pollForResult(state);}

Кстати, кнопка открывается в новой вкладке. Это оказалось удобно: пользователь может пройти авторизацию в основном браузере без переключения VPN, даже если мини-приложение запущено внутри Telegram WebView на телефоне. Или вовсе на другом устройстве — например, на компьютере. После успешного входа вкладка сама закроется, а основной интерфейс получит уведомление через polling. Такая схема пригодилась и в других сценариях — например, для авторизации в системе умного дома через Home Assistant. Один аккаунт — разные каналы управления.

Для Яндекса используем аналогичный OAuth 2.0 flow, но формируем /api/auth/yandex/login на бэкенде.

function startYandexAuth() {    const state = 'pwa_' + crypto.randomUUID();    const authUrl =        `/api/auth/yandex/login?state=${encodeURIComponent(state)}`;    window.open(authUrl, '_blank');    pollForResult(state);}

Сервер обменивает полученный code на access token, получает идентификатор пользователя и сохраняет его. Результат авторизации становится доступен по state через эндпоинт /api/auth/result-by-state. Клиент опрашивает его — например, 120 попыток с интервалом 3 секунды.

function pollForResult(state) {    let attempts = 0;    const poll = async () => {        attempts++;        const resp = await fetch(            `/api/auth/result-by-state?state=${encodeURIComponent(state)}`        );        if (resp.ok) {            const data = await resp.json();            if (data.authenticated && data.code) {                const exchange = await fetch('/api/auth/exchange', {                    method: 'POST',                    headers: { 'Content-Type': 'application/json' },                    credentials: 'include',                    body: JSON.stringify({ code: data.code })                });                if (exchange.ok) {                    loadApp();                    return;                }            }        }        if (attempts < 120) {            setTimeout(poll, 3000);        }    };    poll();}

Здесь важно: в exchange передаётся не OAuth-токен, а одноразовый application-level code. Сервер в ответ устанавливает HttpOnly cookie с сессией. OAuth-токен никогда не попадает в браузер — клиентский JavaScript не может его прочитать, что уменьшает риски при XSS.

state должен быть криптостойким и одноразовым. Math.random() не годится — используем crypto.randomUUID() или crypto.getRandomValues() с энтропией ≥128 бит. На сервере state хранится вместе с code_verifier, имеет короткий TTL и удаляется после успешного callback. При каждом обращении сервер проверяет срок действия и соответствие state конкретной попытке авторизации. Так мы ограничиваем время жизни попытки, связываем колбэк с исходным запросом и исключаем повторное использование state.

С колбэком тоже есть нюанс. Передавать auth_token в URL небезопасно: он может попасть в историю браузера, Referer или серверные/прокси-логи. Лучше использовать короткоживущий code, который клиент обменивает на сессию через POST.

const code = new URLSearchParams(location.search).get('code');if (code) {    const resp = await fetch('/api/auth/exchange', {        method: 'POST',        headers: { 'Content-Type': 'application/json' },        credentials: 'include',        body: JSON.stringify({ code })    });}

Одна и та же страница работает в трёх сценариях: браузер, Telegram WebApp, расширение. Флаг from_extension используется только для UI‑сценариев и не должен восприниматься сервером как признак безопасности. Мы передаём его в URL при открытии страницы из расширения, чтобы корректно обработать поведение интерфейса — например, закрытие вкладки после авторизации или показ специальных сообщений только для расширения.

Для MAX мы используем deep link с одноразовым токеном.

async function startMaxAuth() {    const resp = await fetch('/api/auth/max-token?source=telegram');    const { token, botUsername } = await resp.json();    window.open(`https://max.ru/${botUsername}?start=${token}`, '_blank');    startMaxWaiting(token);}

После привязки бот присылает подтверждение в чат.

Сравнение провайдеров:

Провайдер

Протокол

Особенности в нашей реализации

VK ID

OAuth 2.1 + PKCE

Используем code_challenge S256, передаём device_id

Яндекс ID

OAuth 2.0

Запрашиваем только login:info, email не храним

MAX

Deep link + одноразовый токен

В нашей реализации привязка подтверждается в чате

Отдельно стоит сказать про согласие на обработку данных. Для Telegram WebApp мы используем две галочки: общее согласие и отдельное — на передачу данных в Telegram. Это требование закона, и оно реализовано буквально. В PWA-версии (без Telegram) достаточно одной галочки.

Мы применили эту схему в проекте «Моя АнтиСоцсеть» — агрегаторе с индивидуальной лентой новостей. Проект включает ботов в Telegram, MAX и VK, PWA, браузерные расширения, мобильные приложения, интеграцию с умным домом (Home Assistant) и голосовыми помощниками (Алиса, Сбер, Маруся). Пользователи входят через VK, Яндекс или MAX, а ленту и управление получают в Telegram, MAX, VK и через голос. Всё, что касается state, обмена кодов и опроса сервера, скрыто от глаз пользователя — он видит только кнопки входа и короткое сообщение об успехе.

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

Инфо:

ссылка на оригинал статьи https://habr.com/ru/articles/1072068/