Skip to main content

Обзор

Cabinet поддерживает два способа авторизации через Telegram:
СпособОписаниеКогда использовать
Telegram WidgetIframe-виджет от TelegramСтандартная кнопка авторизации
Telegram OIDCOpenID Connect через oauth.telegram.orgPopup-авторизация, современный метод
Оба способа работают параллельно. Когда TELEGRAM_OIDC_ENABLED=true, Cabinet показывает кастомную кнопку с popup-авторизацией. Иначе — стандартный iframe-виджет Telegram.
Требуется CABINET_ENABLED=true и заполненный CABINET_URL.

Telegram OIDC (рекомендуемый)

OIDC (OpenID Connect) — стандартный протокол авторизации. Telegram реализует его через oauth.telegram.org с поддержкой PKCE. Официальная документация Telegram: Telegram Login — OpenID Connect

Эндпоинты протокола

НазначениеURL
Discoveryhttps://oauth.telegram.org/.well-known/openid-configuration
Авторизацияhttps://oauth.telegram.org/auth
Обмен кода на токенhttps://oauth.telegram.org/token
Публичные ключи (JWKS)https://oauth.telegram.org/.well-known/jwks.json
JavaScript SDKhttps://oauth.telegram.org/js/telegram-login.js?3

Шаг 1: Регистрация приложения в BotFather

  1. Откройте Mini App @BotFather
  2. Выберите вашего бота
  3. Перейдите в Bot SettingsWeb Login
  4. Зарегистрируйте Allowed URLs — укажите все домены, с которых будет выполняться авторизация:
    • Origin кабинета: https://cabinet.example.com
    • Redirect URI (если используете Authorization Code Flow напрямую): https://cabinet.example.com/auth/telegram/callback
Telegram обрабатывает вход только с предварительно зарегистрированных URL. Без регистрации домена авторизация не будет работать.
  1. BotFather отобразит Client ID и Client Secret — сохраните их
Client ID — это числовой ID вашего бота (тот же, что в BOT_TOKEN до двоеточия). Например, для токена 1234567890:AABBCCdd... Client ID = 1234567890.
Рекомендация Telegram: установите аватар бота, совпадающий с логотипом вашего сайта, а имя бота должно отражать связь с сервисом. Пользователь увидит аватар и имя бота при подтверждении авторизации.

Шаг 2: Настройка .env бота

# Включить OIDC авторизацию
TELEGRAM_OIDC_ENABLED=true

# Client ID (числовой ID бота)
TELEGRAM_OIDC_CLIENT_ID=1234567890

# Client Secret от BotFather
TELEGRAM_OIDC_CLIENT_SECRET=your_oidc_client_secret_here
Перезапустите бота:
make reload

Доступные scopes

ScopeОписаниеВозвращаемые claims
openidОбязательный. Уникальный ID и временная меткаsub, iss, iat, exp
profileБазовая информацияname, preferred_username, picture
phoneВерифицированный номер телефона (требует отдельного согласия)phone_number
telegram:bot_accessРазрешение боту отправлять сообщения пользователю после входа
Cabinet запрашивает scopes openid и profile. Scope phone не используется.

Структура id_token

После авторизации Telegram возвращает JWT id_token с подписью RS256. Пример декодированного токена:
{
  "iss": "https://oauth.telegram.org",
  "aud": "1234567890",
  "sub": "1234123412341234123",
  "iat": 1700000000,
  "exp": 1700003600,
  "id": 987654321,
  "name": "John Doe",
  "preferred_username": "johndoe",
  "picture": "https://cdn4.telesco.pe/file/..."
}
ClaimТипОписание
issstringИздатель — всегда https://oauth.telegram.org
audstringАудитория — числовой Client ID бота
substringУникальный идентификатор пользователя
idnumberЧисловой Telegram ID пользователя
namestringИмя пользователя
preferred_usernamestringUsername в Telegram (без @)
picturestringURL аватара
phone_numberstringНомер телефона (только если запрошен scope phone)
iatnumberUnix-время выдачи токена
expnumberUnix-время истечения токена

Как работает (пошагово)

┌──────────┐    ┌──────────────────┐    ┌──────────────────┐    ┌─────────────┐
│  Браузер │    │  Cabinet Frontend│    │   API бота       │    │  Telegram   │
│          │    │                  │    │                  │    │  OIDC       │
└────┬─────┘    └────────┬─────────┘    └────────┬─────────┘    └──────┬──────┘
     │                   │                       │                     │
     │  1. Загрузка     │                       │                     │
     │  страницы входа  │                       │                     │
     │──────────────────>│                       │                     │
     │                   │  2. GET /cabinet/     │                     │
     │                   │  branding/            │                     │
     │                   │  telegram-widget      │                     │
     │                   │──────────────────────>│                     │
     │                   │  {oidc_enabled: true, │                     │
     │                   │   oidc_client_id: "…"}│                     │
     │                   │<──────────────────────│                     │
     │                   │                       │                     │
     │                   │  3. Загрузка SDK      │                     │
     │                   │  telegram-login.js    │                     │
     │                   │─────────────────────────────────────────────>│
     │                   │  4. Telegram.Login     │                     │
     │                   │  .init({client_id})   │                     │
     │                   │                       │                     │
     │  5. Клик по       │                       │                     │
     │  кнопке "Войти"  │                       │                     │
     │──────────────────>│                       │                     │
     │                   │  6. Telegram.Login     │                     │
     │                   │  .open()              │                     │
     │                   │─────────────────────────────────────────────>│
     │                   │                       │                     │
     │  ┌────────────────────────────────────────────────────────────┐ │
     │  │  7. Popup: пользователь подтверждает авторизацию           │ │
     │  └────────────────────────────────────────────────────────────┘ │
     │                   │                       │                     │
     │                   │  8. Callback:          │                     │
     │                   │  {id_token: "eyJ…"}   │                     │
     │                   │<────────────────────────────────────────────│
     │                   │                       │                     │
     │                   │  9. POST /cabinet/    │                     │
     │                   │  auth/telegram/oidc   │                     │
     │                   │  {id_token: "eyJ…"}   │                     │
     │                   │──────────────────────>│                     │
     │                   │                       │  10. GET JWKS       │
     │                   │                       │──────────────────────>│
     │                   │                       │  {keys: [...]}      │
     │                   │                       │<─────────────────────│
     │                   │                       │                     │
     │                   │                       │  11. Валидация JWT: │
     │                   │                       │  - подпись RS256    │
     │                   │                       │  - iss, aud, exp    │
     │                   │                       │  - replay-защита   │
     │                   │                       │                     │
     │                   │  12. {access_token,   │                     │
     │                   │   refresh_token, user}│                     │
     │                   │<──────────────────────│                     │
     │                   │                       │                     │
     │  13. Авторизован  │                       │                     │
     │<──────────────────│                       │                     │
Детали каждого шага:
  1. Пользователь открывает страницу входа Cabinet
  2. Frontend запрашивает конфигурацию виджета с API бота (публичный эндпоинт, без авторизации)
  3. Если oidc_enabled=true — загружается JavaScript SDK Telegram (telegram-login.js?3)
  4. SDK инициализируется с client_id и callback-функцией:
    Telegram.Login.init({
      client_id: 1234567890,
      request_access: ['write'],
      lang: 'ru'
    }, callback)
    
  5. Пользователь нажимает кнопку «Войти через Telegram»
  6. SDK открывает popup-окно авторизации на oauth.telegram.org
  7. Пользователь видит имя и аватар бота, подтверждает вход
  8. Popup закрывается, SDK вызывает callback с {id_token: "eyJ…"} или {error: "..."}
  9. Frontend отправляет id_token на API бота
  10. Бот загружает публичные ключи JWKS (кэшируются на 1 час)
  11. Бот валидирует JWT-подпись и claims
  12. Бот создаёт/находит пользователя, возвращает JWT-токены Cabinet
  13. Пользователь авторизован и перенаправляется на главную страницу

JavaScript SDK

Cabinet использует Telegram Login SDK. Доступные методы:
МетодОписание
Telegram.Login.init(options, callback)Инициализация SDK и регистрация callback
Telegram.Login.open([callback])Открыть popup авторизации
Telegram.Login.auth(options, callback)Открыть popup с явными параметрами
Параметры инициализации (InitOptions):
ПараметрТипОписание
client_idnumberЧисловой ID бота (обязательный)
request_accessstring[]Запрашиваемые разрешения: ['write'] и/или ['phone']
langstringЯзык интерфейса popup (ru, en, es и др.)
noncestringСерверный random-string для защиты от replay-атак
Структура callback:
{
  id_token: "eyJhbGciOiJSUzI1NiIs...",  // JWT (при успехе)
  user: {                                 // Декодированные claims (клиентская копия)
    name: "John Doe",
    preferred_username: "johndoe",
    picture: "https://cdn4.telesco.pe/..."
  },
  error: "POPUP_CLOSED"                   // Строка ошибки (при неудаче)
}
Данные из user в callback — только для предварительного отображения. Для авторизации всегда используйте id_token с серверной валидацией.

Валидация id_token на сервере

Бот выполняет следующие проверки при получении id_token:
  1. Подпись — загружает публичные ключи JWKS, находит ключ по kid из заголовка JWT, проверяет подпись RS256
  2. Издательiss должен быть https://oauth.telegram.org
  3. Аудиторияaud должен совпадать с TELEGRAM_OIDC_CLIENT_ID
  4. Срок действияexp не должен быть в прошлом
  5. Обязательные claims — проверяется наличие exp, iat, iss, aud, sub
  6. Replay-защита — каждый id_token может быть использован только один раз (хэш хранится до 10 минут)

Ротация ключей (JWKS)

Telegram может ротировать ключи подписи. Бот автоматически обновляет JWKS-кэш:
СценарийПоведение
Штатное обновлениеКаждый час (TTL кэша)
Неизвестный kidНемедленное обновление JWKS
Повторный неизвестный kidCooldown 30 секунд между принудительными обновлениями

Rate Limiting

Эндпоинт POST /cabinet/auth/telegram/oidc защищён rate limiter:
  • 10 запросов в 60 секунд на один IP-адрес
  • При превышении — HTTP 429 с заголовком Retry-After
  • Режим fail-closed: при ошибке rate limiter запросы отклоняются

Создание и привязка аккаунтов

При авторизации через OIDC:
  • Существующий пользователь (по Telegram ID) — выполняется вход, обновляются поля first_name, username, last_name из актуальных claims
  • Новый пользователь — создаётся аккаунт с данными из claims. Если передан referral_code или campaign_slug — применяются соответствующие бонусы
  • Неактивный аккаунт — авторизация отклоняется с HTTP 403

Совместимость с другими OIDC-системами

Telegram OIDC совместим со стандартными OIDC-провайдерами и может использоваться с:
  • Keycloak — как внешний Identity Provider
  • Authentik — через Generic OIDC
  • Auth0 — через Custom Social Connection
Telegram не предоставляет отдельный UserInfo endpoint. Все claims содержатся в id_token. Некоторые OIDC-библиотеки могут требовать настройки для пропуска запроса UserInfo.

Telegram Widget (legacy)

Стандартный iframe-виджет Telegram. Используется, когда OIDC отключен (TELEGRAM_OIDC_ENABLED=false).

Настройка домена в BotFather

  1. Откройте @BotFather в Telegram
  2. Выберите вашего бота
  3. Bot SettingsDomain → укажите домен кабинета (например, cabinet.example.com)
Без привязки домена в BotFather виджет авторизации не будет работать. Telegram проверяет, что виджет загружается с авторизованного домена.

Кастомизация виджета

Внешний вид виджета настраивается через переменные окружения или через Admin Cabinet → Настройки:
ПеременнаяОписаниеПо умолчанию
TELEGRAM_WIDGET_SIZEРазмер: large, medium, smalllarge
TELEGRAM_WIDGET_RADIUSСкругление углов (0-20 пикселей)8
TELEGRAM_WIDGET_USERPICПоказывать аватар пользователяtrue
TELEGRAM_WIDGET_REQUEST_ACCESSЗапрашивать write accesstrue
Настройки виджета можно менять без перезапуска бота через Admin Cabinet → Настройки. Значения из базы данных имеют приоритет над переменными окружения.

Как работает

  1. Cabinet загружает iframe-виджет Telegram Login на странице входа
  2. Пользователь нажимает кнопку и подтверждает вход в Telegram
  3. Telegram возвращает данные пользователя (id, first_name, auth_date, hash) с HMAC-SHA256 подписью
  4. Cabinet отправляет данные на API бота
  5. Бот проверяет подпись через SHA256(BOT_TOKEN) и валидирует auth_date (не старше 24 часов)
  6. Бот возвращает JWT-токены для авторизованной сессии

OIDC vs Widget — сравнение

ХарактеристикаOIDCWidget
Метод авторизацииPopup-окноIframe / Redirect
Подпись данныхJWT RS256 (JWKS)HMAC-SHA256 (BOT_TOKEN)
СтандартOpenID ConnectTelegram-проприетарный
Replay-защитаВстроенная (одноразовый токен)Нет (проверка auth_date)
Совместимость с IdPKeycloak, Authentik, Auth0Только Telegram
НастройкаBotFather → Web LoginBotFather → Domain
Кастомизация кнопкиПолная (своя кнопка)Ограничена (iframe)

Конфигурационный API

GET /cabinet/branding/telegram-widget

Публичный эндпоинт (без авторизации). Возвращает конфигурацию для страницы входа. Ответ:
{
  "bot_username": "my_vpn_bot",
  "size": "large",
  "radius": 8,
  "userpic": true,
  "request_access": true,
  "oidc_enabled": true,
  "oidc_client_id": "1234567890"
}
ПолеТипОписание
bot_usernamestringUsername бота (без @)
sizestringРазмер виджета: large, medium, small
radiusnumberСкругление углов (0-20)
userpicbooleanПоказывать аватар
request_accessbooleanЗапрашивать write access
oidc_enabledbooleanВключен ли OIDC
oidc_client_idstringClient ID (пусто если OIDC выключен)
Frontend использует oidc_enabled и oidc_client_id чтобы выбрать способ авторизации: OIDC popup или legacy widget.

POST /cabinet/auth/telegram/oidc

Валидация id_token и авторизация пользователя. Запрос:
{
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "campaign_slug": "summer2025",
  "referral_code": "abc123"
}
ПолеТипОписание
id_tokenstringJWT id_token от Telegram (макс. 4096 символов)
campaign_slugstring?Slug рекламной кампании (опционально)
referral_codestring?Реферальный код (опционально)
Ответ (200):
{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 900,
  "user": {
    "id": 42,
    "telegram_id": 987654321,
    "username": "johndoe",
    "first_name": "John",
    "email": null,
    "balance_kopeks": 0,
    "referral_code": "abc123"
  },
  "campaign_bonus": null
}
Ошибки:
КодПричина
400Telegram OIDC is not configured — OIDC выключен или Client ID не задан
401Invalid or expired Telegram OIDC token — невалидный JWT, истёкший, или повторное использование
401Missing user ID in OIDC claims — нет id или sub в токене
403User account is not active — аккаунт заблокирован
429Rate limit (10 req / 60s per IP)

Итоговая настройка .env

Пример с виджетом и OIDC:
# === CABINET (обязательно) ===
CABINET_ENABLED=true
CABINET_URL=https://cabinet.example.com
CABINET_JWT_SECRET=your_jwt_secret_here
CABINET_ALLOWED_ORIGINS=https://cabinet.example.com

# === TELEGRAM WIDGET (кастомизация) ===
TELEGRAM_WIDGET_SIZE=large
TELEGRAM_WIDGET_RADIUS=8
TELEGRAM_WIDGET_USERPIC=true
TELEGRAM_WIDGET_REQUEST_ACCESS=true

# === TELEGRAM OIDC (рекомендуется) ===
TELEGRAM_OIDC_ENABLED=true
TELEGRAM_OIDC_CLIENT_ID=1234567890
TELEGRAM_OIDC_CLIENT_SECRET=your_oidc_secret

Проверка

API: проверка конфигурации

curl -s https://hooks.example.com/cabinet/branding/telegram-widget | jq
Ответ должен содержать oidc_enabled: true и oidc_client_id с ID бота.

Widget (если OIDC выключен)

  1. Откройте Cabinet в браузере
  2. На экране входа должна появиться iframe-кнопка Telegram
  3. Нажмите — откроется авторизация Telegram
  4. После подтверждения — вход в Cabinet

OIDC (если включен)

  1. На странице входа отображается кастомная кнопка «Войти через Telegram»
  2. При нажатии откроется popup-окно авторизации
  3. После подтверждения popup закроется и пользователь авторизован

Устранение проблем

Кнопка «Войти через Telegram» не появляется

  1. Проверьте конфигурацию: curl -s https://hooks.example.com/cabinet/branding/telegram-widget | jq
  2. Если oidc_enabled: false:
    • Проверьте TELEGRAM_OIDC_ENABLED=true в .env
    • Проверьте что TELEGRAM_OIDC_CLIENT_ID заполнен
    • Перезапустите бота
  3. Если OIDC выключен, но виджет тоже не появляется:
    • Домен кабинета добавлен в BotFather → Bot Settings → Domain?
    • VITE_TELEGRAM_BOT_USERNAME указан верно (без @)?
    • Страница загружается по HTTPS?
  1. URL кабинета зарегистрирован в BotFather → Bot Settings → Web Login → Allowed URLs?
  2. Убедитесь что домен в CABINET_URL совпадает с зарегистрированным

”Telegram OIDC is not configured” (400)

  1. TELEGRAM_OIDC_ENABLED=true в .env?
  2. TELEGRAM_OIDC_CLIENT_ID не пустой?
  3. Перезапустили бота после изменений?

”Invalid or expired Telegram OIDC token” (401)

ПричинаРешение
Неверный Client IDTELEGRAM_OIDC_CLIENT_ID должен совпадать с числовым ID бота
Токен просроченПользователь слишком долго подтверждал — повторите вход
Повторное использованиеКаждый id_token одноразовый — повторите вход
Расхождение часовНастройте NTP на сервере (допуск ±5 минут)

“Unknown kid in id_token” в логах

Telegram ротировал ключи подписи. Бот автоматически обновит JWKS. Если ошибка повторяется:
  1. Проверьте доступность https://oauth.telegram.org/.well-known/jwks.json с вашего сервера:
    curl -s https://oauth.telegram.org/.well-known/jwks.json | jq '.keys | length'
    
  2. Убедитесь что нет блокировки исходящих HTTPS-запросов к oauth.telegram.org
  3. Проверьте DNS-резолвинг:
    dig oauth.telegram.org
    

Rate limit (429)

Эндпоинт OIDC ограничен 10 запросами в 60 секунд на IP. Это защита от брутфорса. В нормальной работе лимит не должен срабатывать. Если срабатывает в production:
  • Проверьте что reverse proxy передаёт реальный IP пользователя (заголовок X-Forwarded-For)
  • Без этого все запросы идут с IP прокси и быстро исчерпывают лимит