惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threat Research - Cisco Blogs
S
Securelist
H
Heimdal Security Blog
Scott Helme
Scott Helme
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Hacker News
The Hacker News
C
CXSECURITY Database RSS Feed - CXSecurity.com
Spread Privacy
Spread Privacy
Cyberwarzone
Cyberwarzone
V
Vulnerabilities – Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
人人都是产品经理
人人都是产品经理
C
Cisco Blogs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Engineering at Meta
Engineering at Meta
Project Zero
Project Zero
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
Cisco Talos Blog
Cisco Talos Blog
Last Week in AI
Last Week in AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
O
OpenAI News
P
Proofpoint News Feed
Google Online Security Blog
Google Online Security Blog
Recent Announcements
Recent Announcements
Hacker News: Ask HN
Hacker News: Ask HN
美团技术团队
Stack Overflow Blog
Stack Overflow Blog
U
Unit 42
P
Privacy International News Feed
Google DeepMind News
Google DeepMind News
G
GRAHAM CLULEY
Apple Machine Learning Research
Apple Machine Learning Research
TaoSecurity Blog
TaoSecurity Blog
S
Security @ Cisco Blogs
C
Check Point Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Jina AI
Jina AI
S
Secure Thoughts
G
Google Developers Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 最新话题
T
Tenable Blog
Latest news
Latest news
I
InfoQ

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Логин через Telegram по-новому: разбираем OIDC-флоу oauth.telegram.org и собираем его на Python
Sergey Natalenko · 2026-05-11 · via Все публикации подряд на Хабре

Средний

9 мин

11K

Я пилил пет-проект — небольшой бэкенд на Litestar — и хотел прикрутить к нему логин через Telegram. Открыл первый попавшийся туториал на GitHub: HMAC от bot-token, /setdomain в BotFather, голые поля юзера в callback. Почти всё, что я нашёл, было про старый виджет telegram.org/js/telegram-widget.js.

Открыл официальную доку — а там уже не виджет, а полноценный OpenID Connect через oauth.telegram.org: JWKS, JWT, claims. Сел разбираться. В итоге собрал PoC — он умеет логинить пользователя через новый OIDC, держит cookie-сессию для HTML-страниц и отдаёт пару access + refresh токенов для JSON API.

Эта статья — пересказ того, что мне самому хотелось бы прочитать в начале: где в потоке данных Telegram, где браузер, где наш бэк, и какие куски нужно реально писать руками. По ходу — туториал: настройка бота в BotFather, локальный тест через ngrok, запуск.

Код примеров — из репозитория https://github.com/andy-takker/tg-auth. Стек: Python 3.13, Litestar, PyJWT, SQLAlchemy 2.0, aiosqlite. Но сам OIDC-флоу со стеком не связан — те же шаги повторяются на FastAPI, Django или чём угодно ещё.


Старый виджет ≠ новый OIDC

Если вы открывали старые туториалы про Telegram Login — забудьте их сразу, потоки разные.

Старый виджет (telegram-widget.js):

  • кнопка вставляется на любой http://-сайт;

  • Telegram возвращает поля юзера прямо в URL или в JS-callback;

  • подлинность проверяется HMAC-SHA256 от токена бота;

  • в BotFather вы прописываете домен через /setdomain.

Если в туториале видите что-то вроде hash = HMAC_SHA256(data_check_string, SHA256(bot_token)) и поля id, first_name, auth_date, hash — это и есть legacy-виджет, закрывайте.

Новый OIDC-флоу (oauth.telegram.org):

  • Telegram стал полноценным OpenID-провайдером;

  • есть /.well-known/openid-configuration, есть JWKS;

  • ID-токен — настоящий JWT, подписанный публичным ключом из JWKS Telegram;

  • бэкенд проверяет подпись по этому ключу, аудиторию (aud), издателя (iss), exp/iat;

  • bot-token при авторизации не используется — он только для Bot API. Не путать его с Client Secret: BotFather в Web Login выдаёт пару Client ID + Client Secret, и Client Secret нужен для manual OIDC code flow. В popup-варианте бэк его не видит, но в полном flow он есть.

Польза от перехода — стандартный протокол, никакой ручной HMAC-проверки legacy-полей и совместимость с любой OIDC-библиотекой. Цена — другой набор настроек в BotFather и обязательный HTTPS на origin’е.

Ещё один нюанс, на который я не сразу обратил внимание. У нового Telegram Login на самом деле два режима:

  1. Login library — браузерный popup через telegram-login.js. Telegram внутри popup-а сам делает Authorization Code Flow с PKCE и возвращает нам уже готовый id_token через postMessage. Бэк только проверяет JWT — никакого Client Secret, никакого PKCE на нашей стороне.

  2. Manual OIDC — обычный Authorization Code Flow: вы сами редиректите юзера на oauth.telegram.org/auth, ловите code в своём callback’е и меняете его на токены через /token (Basic Auth с Client ID + Client Secret).

В этой статье — первый вариант. Он проще и для типового веба его достаточно. Manual flow нужен, если у вас нативный клиент без браузера или хочется держать весь OAuth-обмен в своих руках.


Что мы соберём

Минимальный набор маршрутов:

Метод

Путь

Зачем

GET

/

Страница логина с виджетом Telegram

POST

/api/v1/auth/telegram

Принимает id_token, валидирует, апсертит юзера, ставит cookie + отдаёт access/refresh

POST

/api/v1/auth/refresh

Меняет refresh на новую пару

POST

/api/v1/auth/logout

Чистит cookie

GET

/app

Защищённая HTML-страница, читает юзера по cookie

Cookie-сессия нужна, чтобы HTML-страницы «помнили» юзера между запросами. JWT-пара — для JSON API (мобилка/SPA). Один логин выдаёт сразу обе вещи.

⚠️ Про сами JWT-токены глубоко лезть здесь не буду — в PoC это просто HS256 без хранения в БД. Полноценный refresh-флоу с одноразовостью, family_id и отслеживанием сессий — тема большая и заслуживает отдельной статьи. Здесь упор на сам OIDC-обмен.


Картинка целиком: кто с кем общается

Прежде чем нырять в код, удобно держать в голове ролевую схему. Вот кто участвует и за что отвечает:

Главное, что стоит увидеть на этой схеме: полный OAuth-обмен /auth/token происходит внутри popup-а Telegram. Бэкенд не делает ни одного запроса к oauth.telegram.org/token, не хранит Client Secret, не возится с PKCE и redirect URI. Получает уже готовый id_token, и его остаётся только проверить.


Что происходит при первом логине: пошагово

Главная sequence-диаграмма статьи. Подробно — что и в каком порядке летает по сети.

Что здесь важно:

1. id_token к нам приходит через postMessage, а не через redirect. Никаких ?code=... в URL, никакого редиректа на /callback. Telegram-popup внутри себя проводит полную OAuth-авторизацию и отдаёт нам уже подписанный JWT через window.postMessage. HTTPS на origin’е нужен не из-за postMessage (он как раз для cross-origin общения и сделан), а потому что Telegram пускает в Trusted Origins только HTTPS-схемы.

2. JWKS — это публичные ключи, по которым мы проверяем подпись JWT. Мы их забираем один раз и кэшируем. PyJWKClient из pyjwt это умеет из коробки:

from jwt import PyJWKClient

jwks_client = PyJWKClient(
    "https://oauth.telegram.org/.well-known/jwks.json",
    cache_keys=True,
    lifespan=600,  # 10 минут
)

В моём приложении этот клиент создаётся один раз при старте и инжектится в use-case через DI.

3. Сама валидация — это один вызов jwt.decode с правильными параметрами. Никакого ручного HMAC-а, никаких сравнений строк:

def _verify_telegram_id_token(id_token: str, jwks_client, client_id, issuer):
    if not id_token:
        raise ValueError("Empty id_token")

    signing_key = jwks_client.get_signing_key_from_jwt(id_token).key
    return jwt.decode(
        id_token,
        signing_key,
        algorithms=["RS256", "ES256"],
        audience=client_id,                       # ваш Client ID из BotFather
        issuer=issuer,                            # "https://oauth.telegram.org"
        options={"require": ["iss", "aud", "exp", "iat", "sub"]},
        leeway=30,                                # на рассинхрон часов
    )

Если хоть что-то не сошлось — подпись, audience, истёкший срок — jwt.decode бросит исключение, мы переводим его в 401. Всё.

Тут уместная оговорка. В комментариях обязательно спросят: «зачем руками через PyJWT, когда есть authlib?». И да, для прода authlib часто правильный выбор — умеет полноценный OAuth/OIDC-клиент с обменом code → token, кэширует JWKS, проверяет nonce, дружит со Starlette/FastAPI/Flask/Django. Если у вас в проекте уже не один OIDC-провайдер или планируется manual code flow с PKCE — берите authlib, не пишите это руками. Здесь я сознательно пошёл голым PyJWT, чтобы было видно, какие именно claim’ы и какие проверки происходят — и какие из них Telegram-специфичные (никаких).

Один важный пункт, который в моём PoC не реализован — проверка nonce. В проде это делается так: сервер перед открытием popup-а генерирует случайное значение, кладёт его в свою сессию, передаёт в Telegram.Login.init(...), а после валидации id_token сверяет claim nonce с тем, что лежит в сессии. Это защита от replay: перехваченный когда-то id_token не сработает повторно. Если делаете прод — добавьте в options.require строку "nonce" и сравнивайте.

4. После валидации — делаем upsert юзера. В id_token лежат claim’ы: sub (стабильный OIDC-идентификатор пользователя у Telegram), id (числовой Telegram user id, может присутствовать отдельно), name, preferred_username, picture, phone_number (если юзер дал scope phone). В коде я беру id, а если его нет — фолбэк на sub; так чуть надёжнее, чем завязываться на один из двух. Из этих полей собираем запись TelegramAccount и линкуем к User. Логика такая:

  1. Ищу TelegramAccount по telegram_id → если есть, переиспользую его юзера и обновляю мутабельные поля.

  2. Иначе ищу User по phone_number → если есть, прикрепляю новый TelegramAccount к нему.

  3. Иначе создаю и User, и TelegramAccount.

Шаг 2 — это то, что позволит позже добавить логин по SMS и не получить дублей юзеров, у которых один и тот же телефон, но два аккаунта.


Туториал: что куда нажимать

Теперь пошагово — как поднять всё это локально.

Шаг 1. Создать бота и настроить OIDC в BotFather

Открываете @BotFather, создаёте бота через /newbot (или используете существующего).

Дальше — самое неочевидное. Настройки OIDC живут не в чате с BotFather, а в его mini-app. В чате нажимаете кнопку «Open» (или иконку приложения), внутри переходите в Bot Settings → Web Login.

Там два важных поля:

  • Trusted Origins — добавьте сюда https://<ваш-ngrok-домен>.ngrok-free.app. Только origin: ни пути, ни слеша на конце. Origin’ов можно несколько (например, localhost через прокси и прод).

  • Redirect URIs — для нашего флоу через telegram-login.js оставьте пустым. Это поле нужно только если вы сами реализуете server-side обмен code → token.

Команда /setdomain — это от старого виджета. Для OIDC она не нужна.

Шаг 2. ngrok (или любой HTTPS-туннель)

Telegram пускает в Trusted Origins только HTTPS-схемы, поэтому http://localhost:8000 напрямую не подойдёт — нужен HTTPS-туннель наружу.

Самое простое — ngrok:

ngrok http 8000

Получаете URL вида https://abcd-1234.ngrok-free.app. Этот URL кладёте в Trusted Origins в BotFather. На бесплатном тарифе ngrok домен меняется при каждом перезапуске — придётся обновлять Trusted Origins каждый раз. Если играетесь часто — есть смысл купить статический домен или взять Cloudflare Tunnel.

Шаг 3. .env и client_id

В корне проекта есть .env.dev — копируете в .env и заполняете:

APP_TG_CLIENT_ID=8506301481      # ваш Client ID из BotFather
APP_SECRET_KEY=<32 hex-символа>  # ключ AES для подписи cookie-сессии (16/24/32 символа)
APP_DB_URL=sqlite+aiosqlite:///./tg_auth.db

Секрет для cookie-сессии у меня в Litestar-овском CookieBackendConfig используется как ключ AES — длина строго 16/24/32 байта. Сгенерировать просто:

python3 -c "import secrets; print(secrets.token_hex(16))"  # 32 hex-символа = 32-байтная строка для AES-ключа

Тот же Client ID нужно прописать в data-client-id в tg_auth/presentors/rest/templates/index.html — это атрибут тега <script>, который грузит виджет:

<script async src="https://oauth.telegram.org/js/telegram-login.js?3"
        data-client-id="8506301481"
        data-onauth="onTelegramAuth(data)"
        data-request-access="write phone"></script>
<button class="tg-auth-button" data-style="shine">Sign In with Telegram</button>

Функция onTelegramAuth(data) — наш callback, в неё Telegram передаёт { id_token, user }. Всё, что она делает:

async function onTelegramAuth(data) {
  if (!data || data.error) return;
  const res = await fetch("/api/v1/auth/telegram", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify({ id_token: data.id_token }),
  });
  if (res.ok) window.location.href = "/app";
}

Ничего больше — это весь фронт.

⚠️ Важный момент: в data Telegram кладёт ещё поле user с распакованными полями (имя, аватарка, и так далее). Удобно для UI на фронте, но доверять им нельзя — это та же информация, что и в id_token, только без подписи. Источник истины для бэка — только server-side проверенный id_token. На фронте data.user показывайте, на бэке — игнорируйте.

Шаг 4. Запуск

make develop      # создаёт .venv, ставит зависимости через uv
make migrate      # применяет alembic-миграции
make run          # python -m tg_auth → uvicorn на :8000

В отдельном терминале — ngrok http 8000. Открываете https-URL ngrok’а, кликаете «Sign In with Telegram», подтверждаете в Telegram-приложении, и должны попасть на /app с вашим именем.


А что во второй раз?

Если cookie уже стоит и она валидная — мы вообще не дёргаем Telegram. Поток такой:

Я держу в сессии буквально {"user": {"id": "<uuid>"}} — больше ничего. Имя/телефон тащу из БД на каждый запрос /app. Это даёт приятный side-эффект: если юзера в БД удалили, контроллер /app это видит, чистит cookie и редиректит на /. В коде — буквально пять строк:

sess_user = request.session.get("user")
if not sess_user:
    return Redirect(path="/")

user = await fetch_user_by_id.execute(UserID(UUID(sess_user["id"])))
if user is None:
    request.clear_session()  # self-heal
    return Redirect(path="/")

Access/refresh — пара слов и тизер

Тот же POST /api/v1/auth/telegram после успешного апсерта возвращает и cookie, и пару JWT-токенов:

{
  "user": { "id": "...", "name": "Jane", "phone_number": "+7..." },
  "tokens": {
    "access_token": "eyJ...",
    "refresh_token": "eyJ...",
    "token_type": "Bearer",
    "expires_in": 900
  }
}

Пара нужна для JSON API — мобильное приложение или SPA положит access в Authorization: Bearer ..., а когда тот протухнет — обменяет refresh на новую пару:

В моём PoC это сделано максимально тупо: HS256, общий секрет, type claim различает access/refresh, никакого хранения в БД. Этого хватает, чтобы продемонстрировать обвязку, но в проде так делать нельзя — нет отзыва, нельзя выкинуть конкретного юзера, при утечке refresh-токена злоумышленник без проблем рефрешится дальше.

Полноценный refresh — это одноразовые токены, family_id для детекции переиспользования, журнал сессий в БД. Тема большая, и про неё я хочу написать отдельно. Здесь — сфокусирован на самом OIDC-обмене.


Грабли, на которые я наступил

Popup открывается, но onTelegramAuth не вызывается. Почти всегда одно из двух: (а) origin не добавлен в Trusted Origins в BotFather, или (б) на сайт отдаётся заголовок Cross-Origin-Opener-Policy: same-origin — он блокирует postMessage из popup-а. Litestar по умолчанию его не ставит, но если у вас впереди nginx/CDN — стоит проверить.

401 Invalid id_token: Audience doesn't match. APP_TG_CLIENT_ID в .env не совпал с data-client-id в index.html. Я на это попадался дважды — поправил в одном месте, забыл в другом. В случае с шаблонами это можно решить одной переменной и прокидывать client_id в шаблон с бэкенда, но с полноценным фронтом в отдельной репе надо будет следить за двумя переменными.

401 Invalid id_token: Signature verification failed. Telegram ротировал ключи в JWKS, а у вас они закэшированы. У PyJWKClient я ставил lifespan=600 — то есть кэш в памяти живёт 10 минут. Простой ребут процесса лечит сразу.

ngrok пересоздал домен — ничего не работает. Бесплатный ngrok даёт новый поддомен на каждый старт. Trusted Origins нужно обновлять каждый раз. Лечится либо платным статическим доменом, либо cloudflared tunnel с привязкой к своему домену.

/app редиректит на / сразу после логина. Cookie ссылается на UUID юзера, которого в БД больше нет (типичная история — снёс файл tg_auth.db после логина). Контроллер сам это увидит и почистит сессию. Просто залогиньтесь снова.


Итог

Новый Telegram OIDC — это история про то, что Telegram перестал быть «своей особенной кнопкой» и стал обычным OpenID-провайдером. Вместо HMAC и /setdomain — JWKS, JWT, claims. Кода на бэке стало меньше: один jwt.decode с правильными параметрами вместо ручной проверки подписи. Цены две — обязательный HTTPS на origin’е и настройки в mini-app BotFather, а не в чате.

Если хотите потрогать руками — код тут: https://github.com/andy-takker/tg-auth. README пошагово описывает запуск, а в tests/ лежит AsyncTestClient-овые проверки на весь HTTP-поток, включая невалидный JWT и self-heal стейл-сессии.

В следующей статье разберу, как сделать refresh-токены так, чтобы за них не было стыдно: одноразовость, family_id, журнал сессий в БД и детекция переиспользования.