Все записи
7 мин

Авторизация на сайте через Telegram-бота: deep-link, подтверждение и вход без пароля

telegram-ботывайбкодингBuilding in Public

Если вы хотите сделать авторизацию на сайте через Telegram-бота, то самый понятный путь такой: пользователь жмёт на сайте «Войти через TG», получает уникальную ссылку на вашего бота, открывает её, и в боте ему прилетает сообщение «вы собираетесь авторизоваться» с кнопкой «Подтвердить», он жмёт и веб-версия впускает его без всякого пароля. Бот при этом один и тот же, что обслуживает мини-приложение, что вход на сайт, и логика встроена в уже существующую систему авторизации, а не дублируется поверх неё. Я именно так и сделал в Картаре, и сейчас расскажу, что заработало сразу, а что пришлось доковыривать, потому что один очень неприятный момент я словил не на этапе кода, а уже когда сам нажал кнопку и тупо смотрел на застывший экран.

Почему я вообще решил выкинуть веб-авторизацию

Была у меня обычная веб-авторизация, и она меня бесила, потому что работала как-то криво, и я каждый раз думал «ну вот опять», а вы же знаете это чувство, когда вход в собственный продукт раздражает больше, чем сама фича. И в какой-то момент я просто сказал себе и ассистенту: «Авторизация через тг должна через бота происходить. Щас чет хуйня какая то. Пора на бота завязывать походу». То есть мысль простая: у меня уже есть бот, он живой, он отдельным контейнером крутится и умеет обрабатывать запуск с разными метками вроде реферальных и промо, так зачем мне держать рядом ещё одну хлипкую конструкцию для входа, если можно весь вход прогнать через то, что и так стоит и работает.

И тут важный момент, который я отдельно проговорил, чтобы Claude Code не начал фантазировать: «Встроить надо в существуюущю систему авторизации через тг где можно. Понимаешь? А не городить дубль логики». Вот это, кстати, болезнь любого вайбкодинга, когда ты говоришь нейросети «сделай вход через бота», она с удовольствием напишет тебе новый параллельный механизм, новые таблицы, новый поток, и формально всё будет работать, а по факту у тебя в проекте теперь две системы авторизации, которые однажды разъедутся, и ты будешь час сидеть и думать, почему на сайте ты залогинен, а в приложении нет. Так что я сразу задал рамку: используем то, что есть, расширяем, а не клонируем.

Как устроен сам флоу входа

Если на пальцах, то логика получилась такая. Пользователь на сайте жмёт «Войти через TG», и бэкенд генерит ему уникальную одноразовую ссылку на бота, это и есть тот самый deep-link, по которому Telegram открывает диалог с ботом и передаёт ему метку запуска. Тот же приём с диплинком и одноразовым токеном я отдельно разбирал, когда делал мост из веба в Telegram, чтобы удержать пользователя. Дальше бот понимает, что человек пришёл именно авторизоваться, а не просто потыкать кнопки, и показывает ему сообщение типа «вы собираетесь авторизоваться» с кнопкой подтверждения. Человек жмёт «Подтвердить», и вот как я это себе описывал ещё до реализации: «жмешь подтвердить и бот говорт ты молодец, а в это время веб версия уже впустила тебя». То есть в идеале бот тебя похвалил, а сайт уже параллельно открыл сессию, и ты ничего больше не делаешь.

На бэкенде это легло поверх существующей веб-авторизации, у которой уже был приветственный бонус для новых и защита от повторного использования через хеши логинов, а сессия выдавалась обычной кукой. Тут ничего экзотического: бот подтверждает личность, бэкенд проверяет, что запрос валидный и не повторный, ставит куку, и всё. Самое спорное архитектурное решение было про то, где хранить эти временные токены авторизации, которые живут между «пользователь нажал войти» и «пользователь подтвердил в боте». Напрашивался Redis, потому что это же классика для коротко-живущих штук с TTL, но у меня в этом проекте Redis нет, и я не стал его тащить ради одной таблички. Так что токены легли в Postgres, да, это менее модно, зато не плодит инфраструктуру под одну функцию, а лишний сервис на проде это лишняя точка отказа и лишний контейнер, за которым надо следить.

Отдельная боль, которую я хочу зафиксировать, чтобы вы на ней не споткнулись, это один бот на два окружения, dev и prod. У меня один бот для разработки и один для боевого, и когда я мержу ветку на прод, то прод-авторизация обязана идти через прод-бота, а не случайно дёргать тестового. Звучит как очевидщина, но именно на таких стыках всё и ломается тихо: код вроде один, а бот за ним должен подставляться разный в зависимости от окружения, и если вы это не прокинули через конфиг, то на проде человек жмёт «войти», а ему прилетает в dev-бота, которого он в глаза не видел. Так что вход через бота это не только про красивый deep-link, это ещё и про дисциплину с переменными окружения, иначе ваша красивая авторизация будет работать только у вас на ноуте.

А вот тут оно и сломалось

И всё бы хорошо, код написан, тесты у Claude Code зелёные, я довольный иду проверять руками. Жму на сайте «Войти через TG», открывается бот, прилетает сообщение, жму «Подтвердить», и дальше тишина. На сайте ровно ничего. Как я сам это сформулировал в тот момент: «Ну и че. В бот пришло, я нажал, а на сайте ничего не произошло». Ты сидишь, смотришь на страницу входа, она тебе не реагирует, и первая мысль ну значит весь флоу сломан, токен не дошёл, бэкенд не принял, что-то с куками, надо лезть в логи и разбираться по полной.

А потом я от безысходности просто обновил страницу. И «А вот обновил страницу и вроде как авторизован, да. А я то надеялся на автоматическую перезагрузку страницы». То есть бэкенд-то всё сделал правильно: бот подтвердил, токен проверился, сессия открылась, кука встала. Сломан был не бэкенд, а ожидание на фронте, сайт после нажатия в боте просто не знал, что что-то изменилось, и продолжал стоять как стоял, потому что никто ему не сказал «эй, человек уже подтвердил, релоадись или подтягивай статус». Авторизация подхватывалась только после ручного refresh, а я-то надеялся, что страница сама оживёт в тот самый момент, когда бот говорит «ты молодец».

Вот это, кстати, очень типовая ловушка таких схем с подтверждением во внешнем канале, будь то бот, имейл или пуш. Бэкенд работает асинхронно и идеально, а у фронта нет механизма узнать, что во внешнем мире что-то произошло, он либо должен опрашивать сервер по таймеру и спрашивать «ну что, подтвердили уже?», либо держать живое соединение и ждать сигнала. Если ни того ни другого нет, то технически вы авторизованы, а визуально нет, и пользователь, который не такой упоротый как я и не догадается обновить страницу, просто решит, что у вас ничего не работает, и уйдёт. Самое обидное, что баг тут не в логике входа, а в том крошечном куске, где фронт должен среагировать на уже случившийся факт.

Что я из этого вынес

Первое и главное: когда делаешь авторизацию через стороннее подтверждение, то проектируй не только «как сервер откроет сессию», но и «как браузер об этом узнает», причём второе важнее для ощущения, что фича вообще работает. Можно сколько угодно гордиться чистым бэкендом, но если человек после нажатия видит мёртвый экран, для него фича сломана, точка. У меня бэкенд был готов раньше, чем фронт научился реагировать, и это классический перекос, когда основную работу нейросеть делает быстро, а вот тонкое место на стыке проскакивает мимо.

Второе: Redis не обязателен, и не надо тащить сервис в проект только потому, что «так принято». Временные токены прекрасно полежат в Postgres, особенно если у вас и так десятки-сотни пользователей, а не миллионы, и вы не строите космос раньше времени. Я тут как раз за то, чтобы не раздувать инфраструктуру под каждую мелочь, потому что каждый лишний контейнер это то, что однажды упадёт ночью.

Третье, про честность. Я этот вход через бота не писал руками, я его оркестрировал: задавал рамку, ловил за руку, когда нейросеть хотела сдублировать логику, тестировал сам и сам же нашёл тот самый баг с обновлением страницы, который ни в каких тестах не вылез, потому что тесты проверяли, что сессия открывается, а не что я как живой человек вижу её открытие. И в этом, мне кажется, и есть вся суть разработки с ИИ сегодня: код пишет машина быстро и неплохо, а вот заметить, что «технически работает» и «человек видит результат» это две разные вещи, всё ещё приходится самому. Кстати, на стыке бота и веба у Картары я уже ловил приватный косяк с сессиями устройства, так что эта зона у меня под особым подозрением, и не зря. А как из «нумерология — нахуя?» эта штука доросла до прода за неделю, я отдельно расписывал в истории про неделю в Картаре.

Так что если будете делать вход на сайт через телеграм-бота, deep-link, кнопка «Подтвердить», сессия без пароля, закладывайте сразу, что фронт должен сам подхватить авторизацию после подтверждения, а не ждать, пока пользователь догадается нажать F5. Вот и делайте выводы.