TunnelHunt

Telegram Login локально: OAuth, HTTPS и проверка callback

6 мин чтения

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

Вход через Telegram и возврат пользователя на локальный сервер через HTTPS

Вход через 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 бесплатно уже сегодня и откройте доступ к локальному серверу всего одной командой.

Зарегистрироваться бесплатно →