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

推荐订阅源

小众软件
小众软件
N
News and Events Feed by Topic
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
The Cloudflare Blog
H
Heimdal Security Blog
Schneier on Security
Schneier on Security
Engineering at Meta
Engineering at Meta
Google Online Security Blog
Google Online Security Blog
宝玉的分享
宝玉的分享
AI
AI
The GitHub Blog
The GitHub Blog
MongoDB | Blog
MongoDB | Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
S
Security @ Cisco Blogs
H
Hacker News: Front Page
F
Fortinet All Blogs
博客园_首页
S
Secure Thoughts
N
News and Events Feed by Topic
P
Proofpoint News Feed
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
Spread Privacy
Spread Privacy
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Hugging Face - Blog
Hugging Face - Blog
Hacker News: Ask HN
Hacker News: Ask HN
C
CXSECURITY Database RSS Feed - CXSecurity.com
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
L
LINUX DO - 最新话题
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
Know Your Adversary
Know Your Adversary
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Scott Helme
Scott Helme
P
Privacy & Cybersecurity Law Blog
S
Securelist
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
O
OpenAI News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
PCI Perspectives
PCI Perspectives
L
LangChain Blog
雷峰网
雷峰网
Security Archives - TechRepublic
Security Archives - TechRepublic
V2EX - 技术
V2EX - 技术

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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, журнал сессий в БД и детекция переиспользования.