Telegram Login локально: OAuth, HTTPS и проверка callback
Настраиваем вход через Telegram на localhost: OpenID Connect, HTTPS-туннель, Allowed URLs, state, PKCE и серверная проверка ID token.

Вход через Telegram можно проверить на своём компьютере: браузер открывает приложение по публичному HTTPS-адресу, Telegram возвращает пользователя на callback этого же адреса, а локальный сервер проверяет результат. Туннель нужен для доступного адреса приложения; он не выполняет проверку личности за вас.
Login, webhook и Mini App — разные задачи
| Задача | Что приходит в приложение | Где продолжить |
|---|---|---|
| Вход пользователя на сайт | Код авторизации и затем ID token | Эта инструкция |
| Получение сообщений ботом | Update через POST webhook | Telegram-бот локально |
| Запуск интерфейса внутри Telegram | Данные Mini App и initData |
Telegram Mini App |
Текущий Telegram Login поддерживает OpenID Connect и Authorization Code Flow с PKCE. Старый iframe-виджет с проверкой hash — отдельная интеграция: его правила не подходят для нового JWT. Client Secret для OIDC также не следует подменять токеном Bot API. Официальный Telegram Login.
Если вам нужен telegram_webhook_secret, вы принимаете сообщения бота: секрет генерируется приложением и передаётся в setWebhook. Он не участвует во входе пользователя на сайт. state связывает попытку Login с браузерной сессией, PKCE — код с начавшим вход клиентом. Ни один из них не заменяет Client Secret. Различия и генерация секрета разобраны в инструкции Telegram-бота.
Что такое oauth.telegram.org и где находится callback
В OIDC-примере ниже https://oauth.telegram.org используется для авторизации через Telegram: браузер переходит на его /auth, а сервер приложения обменивает полученный код через /token. Этот адрес задан в переменной ISSUER.
Callback находится на публичном адресе вашего приложения — например, https://YOUR-SUBDOMAIN.tunnelhunt.ru/auth/telegram/callback. После авторизации браузер возвращается туда с code и state. В Allowed URLs добавляются публичный origin и callback приложения; их настройка приведена в шаге 2. Для проверки всей цепочки начинайте вход с главной страницы своего приложения по HTTPS.
1. Подготовьте тестовое приложение
Нужны Python 3.10+, SSH-клиент и отдельный тестовый бот. Используем серверный callback и стандартный браузерный переход, без popup-виджета.
mkdir telegram-login-demo
cd telegram-login-demo
python3 -m venv .venv
source .venv/bin/activate
python -m pip install 'Flask>=3.1,<4' 'requests>=2.32,<3' 'PyJWT[crypto]>=2.10,<3'
На Windows активируйте .venv\Scripts\Activate.ps1. Переменные окружения ниже задаются через $env:ИМЯ = 'значение'.
Во втором терминале создайте адрес для порта 8000:
ssh -R 80:localhost:8000 -p 2222 tunnelhunt.ru
Скопируйте выданное имя. До запуска Flask возможен 502 — локальный обработчик ещё не работает.
2. Зарегистрируйте точный callback
В BotFather откройте настройки тестового бота → Login Widget. В Allowed URLs добавьте origin и callback:
https://YOUR-SUBDOMAIN.tunnelhunt.ru
https://YOUR-SUBDOMAIN.tunnelhunt.ru/auth/telegram/callback
Возьмите Client ID и Client Secret из этих настроек. Telegram принимает заранее зарегистрированные адреса. Путь и завершающий слеш callback должны совпадать с redirect_uri приложения. Старый /setdomain не заменяет настройку нового OIDC-клиента. Регистрация Allowed URLs.
В первом терминале задайте конфигурацию:
export PUBLIC_URL='https://YOUR-SUBDOMAIN.tunnelhunt.ru'
export TELEGRAM_CLIENT_ID='CLIENT_ID_ИЗ_BOTFATHER'
export TELEGRAM_CLIENT_SECRET='CLIENT_SECRET_ИЗ_BOTFATHER'
export FLASK_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
Client Secret остаётся в окружении сервера. Не добавляйте его в frontend или в Git.
3. Создайте сервер с state и PKCE
Сохраните как app.py:
import base64
import hashlib
import hmac
import os
import secrets
import time
from urllib.parse import urlencode, urlsplit
import jwt
import requests
from flask import Flask, redirect, render_template_string, request, session
app = Flask(__name__)
app.secret_key = os.environ['FLASK_SECRET_KEY']
app.config.update(SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax')
CLIENT_ID = os.environ['TELEGRAM_CLIENT_ID']
CLIENT_SECRET = os.environ['TELEGRAM_CLIENT_SECRET']
ORIGIN = os.environ['PUBLIC_URL'].rstrip('/')
parsed = urlsplit(ORIGIN)
if (parsed.scheme != 'https' or not parsed.hostname or parsed.path
or parsed.query or parsed.fragment or parsed.username):
raise ValueError('PUBLIC_URL must be an HTTPS origin without a path')
CALLBACK = ORIGIN + '/auth/telegram/callback'
ISSUER = 'https://oauth.telegram.org'
keys = jwt.PyJWKClient(ISSUER + '/.well-known/jwks.json', timeout=10)
@app.get('/')
def index():
return render_template_string('''
<h1>Telegram Login на localhost</h1>
{% if subject %}<p>Проверенный sub: {{ subject }}</p>{% endif %}
<a href="/login">Войти через Telegram</a>
''', subject=session.get('subject'))
@app.get('/login')
def login():
verifier = secrets.token_urlsafe(48)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode('ascii')).digest()
).rstrip(b'=').decode('ascii')
state = secrets.token_urlsafe(32)
session.clear()
session['oauth'] = {'state': state, 'verifier': verifier, 'started': time.time()}
query = urlencode({
'client_id': CLIENT_ID, 'redirect_uri': CALLBACK,
'response_type': 'code', 'scope': 'openid profile',
'state': state, 'code_challenge': challenge,
'code_challenge_method': 'S256',
})
return redirect(ISSUER + '/auth?' + query)
@app.get('/auth/telegram/callback')
def callback():
flow = session.pop('oauth', None)
states, codes = request.args.getlist('state'), request.args.getlist('code')
if (not flow or len(states) != 1 or len(codes) != 1
or not codes[0] or len(codes[0]) > 4096
or not hmac.compare_digest(flow['state'].encode(), states[0].encode())
or not 0 <= time.time() - flow['started'] <= 600):
return 'Invalid or expired login. Start again.', 400
try:
response = requests.post(
ISSUER + '/token',
auth=(CLIENT_ID, CLIENT_SECRET), timeout=10,
data={'grant_type': 'authorization_code', 'code': codes[0],
'redirect_uri': CALLBACK, 'client_id': CLIENT_ID,
'code_verifier': flow['verifier']},
)
response.raise_for_status()
tokens = response.json()
if not isinstance(tokens, dict) or not isinstance(tokens.get('id_token'), str):
return 'Invalid token response', 502
token = tokens['id_token']
signing_key = keys.get_signing_key_from_jwt(token).key
claims = jwt.decode(
token, signing_key, algorithms=['RS256'],
issuer=ISSUER, audience=CLIENT_ID,
options={'require': ['exp', 'iat', 'iss', 'aud', 'sub']},
)
except (requests.RequestException, ValueError, jwt.PyJWTError):
return 'Login verification failed. Start again.', 400
if not isinstance(claims['sub'], str) or not claims['sub']:
return 'Invalid subject', 400
session.clear()
session['subject'] = claims['sub']
return redirect('/')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000, debug=False)
Запустите python app.py и откройте публичный HTTPS-адрес. Не начинайте вход с http://localhost:8000: secure-cookie и сессия callback относятся к публичному origin. Используйте тот же браузер до завершения входа.
Пример рассчитан на стандартный RS256 в настройках Telegram. Проверка подписи использует публичные ключи JWKS, затем проверяет issuer, audience и срок токена. Нельзя просто декодировать JWT без проверки. Примеры проверки JWT и JWKS в PyJWT.
4. Проверьте результат
Нажмите «Войти через Telegram», подтвердите вход в тестового бота и дождитесь возврата. После callback сервер должен перенаправить на /, где появится проверенный sub. Не ожидайте сообщение в webhook бота: это другой канал.
Для диагностики смотрите цепочку в Network: переход к авторизации → callback с code и state → редирект на главную. Обмен кода на токен выполняется сервером. Authorization code в URL и журналы callback не публикуйте: они содержат временные данные входа.
| Проблема | Что проверить |
|---|---|
| Telegram отклоняет адрес | Allowed URLs, точный путь и origin, текущий поддомен |
Invalid or expired login |
Cookie, тот же браузер, срок 10 минут, новый вход после перезапуска |
Login verification failed |
Client ID/Secret, доступ к token/JWKS, часы компьютера, алгоритм RS256 |
| 502 от туннеля | Flask работает на 8000, SSH-сессия ещё активна |
| Повтор старого callback не работает | Это ожидаемо: начните новую попытку входа |
При ошибке callback сначала сравните три адреса целиком: зарегистрированный Allowed URL, CALLBACK в сервере и redirect_uri запроса авторизации в Network. Схема HTTPS, hostname, путь и завершающий слеш должны совпадать. После смены домена обновите настройки, перезапустите Flask и начните вход заново с /login, а не с сохранённого callback. Если cookie отсутствует, убедитесь, что вход начат с публичного HTTPS-origin в том же браузере; не отключайте Secure ради исправления сессии.
Как избежать смены callback
Временный адрес подходит для первой проверки, но после изменения имени нужно обновить Allowed URLs и PUBLIC_URL, затем перезапустить Flask. Закреплённый поддомен Pro позволяет сохранить callback между подключениями. Компьютер и SSH всё равно должны работать.
Этот пример демонстрирует проверку входа, а не готовую систему аккаунтов: для продукта понадобятся постоянная база пользователей, управление сессиями и logout. Вход с настоящим Telegram требует собственных настроек BotFather; отдельно можно тестировать негативные сценарии с искусственными JWT. Продолжить настройку адреса поможет руководство по localhost.
Проверьте Telegram Login на своём localhost
Начните с тестового бота и HTTPS-туннеля. Для повторных входов закрепите поддомен на Pro, чтобы сохранить callback и Allowed URLs между подключениями.
Первый запуск: добавьте публичный SSH-ключ, запустите локальное приложение и команду SSH из статьи, затем откройте выданный HTTPS-адрес. Если ответа нет, пройдите проверку localhost и туннеля.
Free: 1 ГБ трафика в месяц, 1 туннель, сессия до 4 часов. Pro: 199 ₽/месяц, 50 ГБ, до 5 туннелей и резервирование поддомена. Компьютер, приложение и SSH-соединение должны работать; постоянный адрес сохраняет имя, а не доступность выключенного сервера.
Готовы запустить свой первый туннель?
Начните использовать TunnelHunt бесплатно уже сегодня и откройте доступ к локальному серверу всего одной командой.
Зарегистрироваться бесплатно →По теме

Telegram-бот локально: webhook, secret_token и отладка на localhost
5 июня 2025 г.
Запустите Telegram-бота на localhost: выберите polling или webhook, настройте HTTPS и secret_token, проверьте getWebhookInfo и исправьте ошибки доставки.

Telegram Mini App локально: Vite, HTTPS и запуск с телефона
12 сентября 2026 г.
Создаём минимальный Telegram Mini App на Vite: публичный HTTPS, allowedHosts, кнопка BotFather, локальный API и проверка ошибок на телефоне.

Как открыть localhost в интернете: публичный HTTPS через SSH
11 сентября 2026 г.
Пошагово открываем локальный сайт из интернета без настройки роутера: SSH-туннель, проверка ссылки, Docker, Vite и диагностика ошибок.

Vite через HTTPS-туннель: allowedHosts, Blocked request и HMR
5 октября 2026 г.
Открываем React или Vue на Vite через SSH: точный allowedHosts, фиксированный порт, WebSocket HMR и локальный API без лишнего CORS.