TMA Auth Architecture
Архитектурное решение по авторизации в Telegram Mini App.
1. Summary
Решение: Используем initData + HMAC как единственный механизм авторизации TMA. JWT не используется.
Причина: Telegram Mini App имеет специфичный flow, где JWT добавляет complexity без пропорциональной выгоды.
JWT для TMA — overengineering. initData достаточно для безопасной авторизации.
2. ADR (Architectural Decision Record)
Контекст
При проектировании авторизации TMA рассматривались два подхода:
| Подход | Описание |
|---|---|
| initData only | Telegram initData с HMAC валидацией на каждый запрос |
| JWT + Refresh | initData только для login, далее JWT токены |
Решение
Выбран подход: initData only.
Обоснование
1. Типичный user flow
Пользователь:
1. Открывает TMA через бота/канал
2. Играет 10-30 минут
3. Закрывает TMA
4. Позже открывает снова → НОВЫЙ initData
Ключевой момент: Каждое открытие TMA = новый initData с новым auth_date. Проблема "сессия устарела" возникает только если держать приложение открытым >24 часов — это edge case.
2. initData vs JWT — сравнение
| Критерий | initData only | JWT + Refresh |
|---|---|---|
| Сложность | ~200 LOC | ~500+ LOC |
| Безопасность | ✅ HMAC + auth_date | ✅ JWT expiry + refresh |
| UX при истечении | "Закрой и открой" | Seamless refresh |
| Когда истекает | При открытом >24h | access: 15min, refresh: 24h |
| Частота проблемы | Редко (edge case) | Никогда |
3. Почему JWT избыточен
- Новый заход = новый initData — проблема истечения не возникает при нормальном использовании
- HMAC достаточен — Telegram гарантирует подлинность данных
- auth_date защищает от replay — старые initData отклоняются
- Complexity не оправдана — JWT требует:
- 2 новых endpoint (
/auth/login,/auth/refresh) - Хранение refresh token (cookie/storage)
- Логика refresh на frontend
- Обработка race conditions при refresh
- 2 новых endpoint (
Последствия
Положительные:
- Простая архитектура
- Меньше кода для поддержки
- Меньше точек отказа
Отрицательные:
- При открытом TMA >24h — экран "Сессия устарела"
- Пользователь должен переоткрыть через Telegram
Mitigation: Это ожидаемое поведение, не баг. Экран информирует пользователя что делать.
3. Technical Implementation
Flow авторизации
Валидация initData
Файл: backend/src/domains/telegram/utils/telegram-init-data.util.ts
1. Проверка HMAC подписи (crypto.timingSafeEqual)
2. Проверка auth_date:
- Production: до 48h (с warning до 72h)
- Development: 48h строго
3. Парсинг user JSON
4. Валидация telegramId
TTL настройки
Существуют две независимые проверки auth_date с разными TTL:
| Контекст | TTL | Источник | Файл |
|---|---|---|---|
| Основной middleware (все запросы) | 48h hardcoded (до 72h с warning в prod) | maxAge = 48 * 60 * 60 | telegram-init-data.util.ts:62 |
activate-session (onboarding) | 24h | TIME_CONSTANTS.MAX_AUTH_TIMESTAMP_AGE | telegram-bot.routes.ts:487, time.constants.ts:32 |
Это не дублирование. Основной middleware в telegram-init-data.util.ts проверяет auth_date с hardcoded 48h TTL (с grace period до 72h в production). Отдельно, activate-session endpoint в telegram-bot.routes.ts использует TIME_CONSTANTS.MAX_AUTH_TIMESTAMP_AGE (24h) — более строгую проверку для onboarding (UTM/referral). activate-session вызывается однократно при первом входе нового пользователя.
4. Когда показывается "Сессия устарела"
Условия
Сессия может быть отклонена в двух сценариях:
- Любой аутентифицированный запрос через основной middleware — если
auth_dateстарше 48h (или >72h в production с grace period). Middleware вtelegram-init-data.util.tsотклоняет initData, что приводит к ошибке авторизации. activate-session(onboarding) — еслиauth_dateстарше 24h (TIME_CONSTANTS.MAX_AUTH_TIMESTAMP_AGE). Более строгий порог для одноразовой активации UTM/referral сессий.
Почему это не проблема
| Сценарий | Результат |
|---|---|
| Обычный пользователь (играет 20 мин) | Работает |
| Закрыл и открыл через час | Новый initData |
| Закрыл и открыл через день | Новый initData |
Держал открытым 25 часов, делает activate-session | "Сессия устарела" (24h TTL) |
| Держал открытым 49 часов, любой запрос | "Сессия устарела" (48h TTL) |
| Держал открытым 49-72 часа в production | Warning в логах, запросы проходят (grace period) |
| Держал открытым >72 часов в production | "Сессия устарела" |
Частота проблемы: менее 1% пользователей.
UI экран
Файл: frontend/src/components/SessionExpiredScreen.tsx
Показывает:
- Иконку часов
- "Сессия устарела"
- "Закройте и откройте заново через Telegram"
- Кнопку "Закрыть приложение"
5. Сравнение с альтернативами
Почему НЕ JWT
| Аргумент за JWT | Контраргумент |
|---|---|
| "initData не для долгой сессии" | Сессия 20 мин, не сутки |
| "UX ломается при свернул/вернулся" | Новый заход = новый initData |
| "Риск replay attack" | HMAC + auth_date достаточно |
| "Стандарт индустрии" | Для web apps, не для TMA |
Когда JWT имел бы смысл
- Offline режим (кэширование данных)
- Cross-platform (web + mobile native)
- Long-running background tasks
goLoot не имеет этих требований.
6. FAQ
Q: Что если пользователь свернул TMA и вернулся?
A: initData остаётся в памяти. Если прошло <48h — работает. Если >48h — показываем экран "устарела".
Но на практике: пользователь закроет TMA раньше, при следующем открытии получит новый initData.
Q: Можно ли обновить initData без переоткрытия?
A: Нет. Telegram обновляет initData только при открытии Mini App. Это ограничение платформы.
Q: Почему TTL 48h, а не 24h?
A: 24h было слишком строго для пользователей в разных timezone. 48h даёт buffer для edge cases без ущерба безопасности.
Q: Как защищаемся от replay attack?
A:
- HMAC подпись — initData нельзя подделать
- auth_date — старые initData отклоняются
- HTTPS — трафик зашифрован
- Rate limiting — защита от брутфорса
7. Related
- Security Matrix — полная матрица защит
- Telegram Bot — Telegram интеграция
- Referrals — реферальная система (InviteSession)
- UTM Tracking — UTM tracking (InviteSession)
8. References
Код:
backend/src/domains/telegram/middleware/telegram-auth.middleware.tsbackend/src/domains/telegram/utils/telegram-init-data.util.tsbackend/src/common/constants/time.constants.tsfrontend/src/services/api-client.tsfrontend/src/components/SessionExpiredScreen.tsx
Telegram Docs: