From f9f9195e140796e08293519a20890f53e4bdaa5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Wed, 20 May 2026 13:00:47 +0300 Subject: [PATCH] fix: Telegram Mini App auth via initData (no web login needed) - Validate Telegram WebApp initData using HMAC-SHA256 - Dual auth: cookie (web panel) OR X-Telegram-Init-Data header - Mini App sends initData with every API request - Retry button on auth failure instead of dead end Co-Authored-By: Claude Opus 4.6 --- server/auth.py | 54 ++++++++++++++++++++++++++++-- server/templates/telegram_app.html | 19 +++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/server/auth.py b/server/auth.py index ae410f5..bccd3ee 100644 --- a/server/auth.py +++ b/server/auth.py @@ -1,4 +1,9 @@ +import hashlib +import hmac +import json from datetime import datetime, timedelta +from urllib.parse import parse_qs + from jose import jwt from passlib.context import CryptContext from fastapi import Request, HTTPException @@ -29,11 +34,54 @@ def verify_token(token: str) -> str | None: return None +def verify_telegram_init_data(init_data: str) -> bool: + """Validate Telegram WebApp initData using bot token.""" + settings = load_settings() + bot_token = settings.get("telegram_bot_token", "") + if not bot_token: + return False + + try: + parsed = parse_qs(init_data) + received_hash = parsed.get("hash", [""])[0] + if not received_hash: + return False + + # Build data-check-string + data_pairs = [] + for key, values in parsed.items(): + if key != "hash": + data_pairs.append(f"{key}={values[0]}") + data_pairs.sort() + data_check_string = "\n".join(data_pairs) + + # HMAC-SHA256 + secret_key = hmac.new( + b"WebAppData", bot_token.encode(), hashlib.sha256 + ).digest() + calculated_hash = hmac.new( + secret_key, data_check_string.encode(), hashlib.sha256 + ).hexdigest() + + return calculated_hash == received_hash + except Exception: + return False + + def get_current_user(request: Request) -> str | None: + # 1. Cookie auth token = request.cookies.get("access_token") - if not token: - return None - return verify_token(token) + if token: + user = verify_token(token) + if user: + return user + + # 2. Telegram initData auth (header) + tg_init = request.headers.get("X-Telegram-Init-Data") + if tg_init and verify_telegram_init_data(tg_init): + return "telegram_user" + + return None def require_auth(request: Request): diff --git a/server/templates/telegram_app.html b/server/templates/telegram_app.html index b128953..14bb698 100644 --- a/server/templates/telegram_app.html +++ b/server/templates/telegram_app.html @@ -277,9 +277,21 @@ setTimeout(() => el.classList.remove('show'), 2500); } - // Fetch with auth + // Telegram initData for auth + const initData = tg?.initData || ''; + + // Fetch with auth (cookie or Telegram initData) async function api(url, opts = {}) { - const resp = await fetch(url, {credentials: 'include', ...opts}); + const headers = opts.headers || {}; + if (initData) { + headers['X-Telegram-Init-Data'] = initData; + } + const resp = await fetch(url, { + credentials: 'include', + ...opts, + headers + }); + if (!resp.ok) throw new Error(resp.status); return resp.json(); } @@ -292,7 +304,8 @@ document.getElementById('app').innerHTML = `
🔒 - Авторизуйтесь в веб-панели + Ошибка загрузки. Попробуйте /start +
`; } }