> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bedolagam.ru/llms.txt
> Use this file to discover all available pages before exploring further.

# Telegram OIDC авторизация

> Настройка входа через Telegram OpenID Connect и виджет авторизации

## Обзор

Cabinet поддерживает два способа авторизации через Telegram:

| Способ | Описание | Когда использовать |
| - | - | - |
| **Telegram Widget** | Iframe-виджет от Telegram | Стандартная кнопка авторизации |
| **Telegram OIDC** | OpenID Connect через `oauth.telegram.org` | Popup-авторизация, современный метод |

Оба способа работают параллельно. Когда `TELEGRAM_OIDC_ENABLED=true`, Cabinet показывает кастомную кнопку с popup-авторизацией. Иначе — стандартный iframe-виджет Telegram.

> [!WARNING]
>
> Требуется `CABINET_ENABLED=true` и заполненный `CABINET_URL`.

***

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

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

**Официальная документация Telegram:** [Telegram Login — OpenID Connect](https://core.telegram.org/bots/telegram-login#openid-connect)

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

| Назначение | URL |
| - | - |
| Discovery | `https://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 SDK | `https://oauth.telegram.org/js/telegram-login.js?3` |

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

1. Откройте Mini App [@BotFather](https://t.me/BotFather)
2. Выберите вашего бота
3. Перейдите в **Bot Settings** → **Web Login**
4. Зарегистрируйте **Allowed URLs** — укажите все домены, с которых будет выполняться авторизация:
   * Origin кабинета: `https://cabinet.example.com`
   * Redirect URI (если используете Authorization Code Flow напрямую): `https://cabinet.example.com/auth/telegram/callback`

> [!WARNING]
>
> Telegram обрабатывает вход **только** с предварительно зарегистрированных URL. Без регистрации домена авторизация не будет работать.

5. BotFather отобразит **Client ID** и **Client Secret** — сохраните их

> [!NOTE]
>
> **Client ID** — это числовой ID вашего бота (тот же, что в `BOT_TOKEN` до двоеточия). Например, для токена `1234567890:AABBCCdd...` Client ID = `1234567890`.

> [!TIP]
>
> Рекомендация Telegram: установите аватар бота, совпадающий с логотипом вашего сайта, а имя бота должно отражать связь с сервисом. Пользователь увидит аватар и имя бота при подтверждении авторизации.

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

```env theme={null}
# Включить 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
```

Перезапустите бота:

```bash theme={null}
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. Пример декодированного токена:

```json theme={null}
{
  "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 | Тип | Описание |
| - | - | - |
| `iss` | string | Издатель — всегда `https://oauth.telegram.org` |
| `aud` | string | Аудитория — числовой Client ID бота |
| `sub` | string | Уникальный идентификатор пользователя |
| `id` | number | Числовой Telegram ID пользователя |
| `name` | string | Имя пользователя |
| `preferred_username` | string | Username в Telegram (без `@`) |
| `picture` | string | URL аватара |
| `phone_number` | string | Номер телефона (только если запрошен scope `phone`) |
| `iat` | number | Unix-время выдачи токена |
| `exp` | number | Unix-время истечения токена |

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

```
┌──────────┐    ┌──────────────────┐    ┌──────────────────┐    ┌─────────────┐
│  Браузер │    │  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-функцией:
   ```javascript theme={null}
   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_id` | number | Числовой ID бота (обязательный) |
| `request_access` | string\[] | Запрашиваемые разрешения: `['write']` и/или `['phone']` |
| `lang` | string | Язык интерфейса popup (`ru`, `en`, `es` и др.) |
| `nonce` | string | Серверный random-string для защиты от replay-атак |

**Структура callback:**

```javascript theme={null}
{
  id_token: "eyJhbGciOiJSUzI1NiIs...",  // JWT (при успехе)
  user: {                                 // Декодированные claims (клиентская копия)
    name: "John Doe",
    preferred_username: "johndoe",
    picture: "https://cdn4.telesco.pe/..."
  },
  error: "POPUP_CLOSED"                   // Строка ошибки (при неудаче)
}
```

> [!WARNING]
>
> Данные из `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 |
| Повторный неизвестный `kid` | Cooldown 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

> [!NOTE]
>
> Telegram **не предоставляет** отдельный UserInfo endpoint. Все claims содержатся в `id_token`. Некоторые OIDC-библиотеки могут требовать настройки для пропуска запроса UserInfo.

***

## Telegram Widget (legacy)

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

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

1. Откройте [@BotFather](https://t.me/BotFather) в Telegram
2. Выберите вашего бота
3. **Bot Settings** → **Domain** → укажите домен кабинета (например, `cabinet.example.com`)

> [!WARNING]
>
> Без привязки домена в BotFather виджет авторизации **не будет работать**. Telegram проверяет, что виджет загружается с авторизованного домена.

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

Внешний вид виджета настраивается через переменные окружения или через Admin Cabinet → Настройки:

| Переменная | Описание | По умолчанию |
| - | - | - |
| `TELEGRAM_WIDGET_SIZE` | Размер: `large`, `medium`, `small` | `large` |
| `TELEGRAM_WIDGET_RADIUS` | Скругление углов (0-20 пикселей) | `8` |
| `TELEGRAM_WIDGET_USERPIC` | Показывать аватар пользователя | `true` |
| `TELEGRAM_WIDGET_REQUEST_ACCESS` | Запрашивать write access | `true` |

> [!NOTE]
>
> Настройки виджета можно менять без перезапуска бота через 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 — сравнение

| Характеристика | OIDC | Widget |
| - | - | - |
| Метод авторизации | Popup-окно | Iframe / Redirect |
| Подпись данных | JWT RS256 (JWKS) | HMAC-SHA256 (BOT\_TOKEN) |
| Стандарт | OpenID Connect | Telegram-проприетарный |
| Replay-защита | Встроенная (одноразовый токен) | Нет (проверка auth\_date) |
| Совместимость с IdP | Keycloak, Authentik, Auth0 | Только Telegram |
| Настройка | BotFather → Web Login | BotFather → Domain |
| Кастомизация кнопки | Полная (своя кнопка) | Ограничена (iframe) |

***

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

### GET /cabinet/branding/telegram-widget

Публичный эндпоинт (без авторизации). Возвращает конфигурацию для страницы входа.

**Ответ:**

```json theme={null}
{
  "bot_username": "my_vpn_bot",
  "size": "large",
  "radius": 8,
  "userpic": true,
  "request_access": true,
  "oidc_enabled": true,
  "oidc_client_id": "1234567890"
}
```

| Поле | Тип | Описание |
| - | - | - |
| `bot_username` | string | Username бота (без `@`) |
| `size` | string | Размер виджета: `large`, `medium`, `small` |
| `radius` | number | Скругление углов (0-20) |
| `userpic` | boolean | Показывать аватар |
| `request_access` | boolean | Запрашивать write access |
| `oidc_enabled` | boolean | Включен ли OIDC |
| `oidc_client_id` | string | Client ID (пусто если OIDC выключен) |

Frontend использует `oidc_enabled` и `oidc_client_id` чтобы выбрать способ авторизации: OIDC popup или legacy widget.

### POST /cabinet/auth/telegram/oidc

Валидация id\_token и авторизация пользователя.

**Запрос:**

```json theme={null}
{
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "campaign_slug": "summer2025",
  "referral_code": "abc123"
}
```

| Поле | Тип | Описание |
| - | - | - |
| `id_token` | string | JWT id\_token от Telegram (макс. 4096 символов) |
| `campaign_slug` | string? | Slug рекламной кампании (опционально) |
| `referral_code` | string? | Реферальный код (опционально) |

**Ответ (200):**

```json theme={null}
{
  "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
}
```

**Ошибки:**

| Код | Причина |
| - | - |
| 400 | `Telegram OIDC is not configured` — OIDC выключен или Client ID не задан |
| 401 | `Invalid or expired Telegram OIDC token` — невалидный JWT, истёкший, или повторное использование |
| 401 | `Missing user ID in OIDC claims` — нет `id` или `sub` в токене |
| 403 | `User account is not active` — аккаунт заблокирован |
| 429 | Rate limit (10 req / 60s per IP) |

***

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

Пример с виджетом и OIDC:

```env theme={null}
# === 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: проверка конфигурации

```bash theme={null}
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?

### Popup открывается, но сразу закрывается

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 ID | `TELEGRAM_OIDC_CLIENT_ID` должен совпадать с числовым ID бота |
| Токен просрочен | Пользователь слишком долго подтверждал — повторите вход |
| Повторное использование | Каждый id\_token одноразовый — повторите вход |
| Расхождение часов | Настройте NTP на сервере (допуск ±5 минут) |

### "Unknown kid in id\_token" в логах

Telegram ротировал ключи подписи. Бот автоматически обновит JWKS.

Если ошибка повторяется:

1. Проверьте доступность `https://oauth.telegram.org/.well-known/jwks.json` с вашего сервера:
   ```bash theme={null}
   curl -s https://oauth.telegram.org/.well-known/jwks.json | jq '.keys | length'
   ```
2. Убедитесь что нет блокировки исходящих HTTPS-запросов к `oauth.telegram.org`
3. Проверьте DNS-резолвинг:
   ```bash theme={null}
   dig oauth.telegram.org
   ```

### Rate limit (429)

Эндпоинт OIDC ограничен 10 запросами в 60 секунд на IP. Это защита от брутфорса. В нормальной работе лимит не должен срабатывать.

Если срабатывает в production:

* Проверьте что reverse proxy передаёт реальный IP пользователя (заголовок `X-Forwarded-For`)
* Без этого все запросы идут с IP прокси и быстро исчерпывают лимит


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.