112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
|
|
"""
|
|||
|
|
MAX WebApp (Mini App) auth helper.
|
|||
|
|
|
|||
|
|
Валидация initData от MAX Bridge — тот же алгоритм, что и у Telegram:
|
|||
|
|
secret_key = HMAC_SHA256("WebAppData", BOT_TOKEN), data_check_string без hash, сравнение с hash.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import hashlib
|
|||
|
|
import hmac
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
from typing import Dict, Any
|
|||
|
|
from urllib.parse import parse_qsl
|
|||
|
|
|
|||
|
|
from ..config import settings
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MaxAuthError(Exception):
|
|||
|
|
"""Ошибка проверки подлинности MAX initData."""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_init_data(init_data: str) -> Dict[str, Any]:
|
|||
|
|
"""Разбирает строку initData (query string) в словарь."""
|
|||
|
|
data: Dict[str, Any] = {}
|
|||
|
|
for key, value in parse_qsl(init_data, keep_blank_values=True):
|
|||
|
|
data[key] = value
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
|
|||
|
|
def verify_max_init_data(init_data: str) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Проверяет подпись initData по правилам MAX (аналогично Telegram).
|
|||
|
|
|
|||
|
|
- secret_key = HMAC_SHA256("WebAppData", BOT_TOKEN)
|
|||
|
|
- data_check_string: пары key=value без hash, сортировка по key, разделитель \n
|
|||
|
|
- hex(HMAC_SHA256(secret_key, data_check_string)) === hash из initData
|
|||
|
|
"""
|
|||
|
|
if not init_data:
|
|||
|
|
logger.warning("[MAX] verify_max_init_data: init_data пустой")
|
|||
|
|
raise MaxAuthError("init_data is empty")
|
|||
|
|
|
|||
|
|
bot_token = (getattr(settings, "max_bot_token", None) or "").strip()
|
|||
|
|
if not bot_token:
|
|||
|
|
logger.warning("[MAX] MAX_BOT_TOKEN не задан в .env")
|
|||
|
|
raise MaxAuthError("MAX bot token is not configured")
|
|||
|
|
|
|||
|
|
parsed = _parse_init_data(init_data)
|
|||
|
|
logger.info("[MAX] initData распарсен, ключи: %s", list(parsed.keys()))
|
|||
|
|
|
|||
|
|
received_hash = parsed.pop("hash", None)
|
|||
|
|
if not received_hash:
|
|||
|
|
logger.warning("[MAX] В initData отсутствует поле hash")
|
|||
|
|
raise MaxAuthError("Missing hash in init_data")
|
|||
|
|
|
|||
|
|
data_check_items = []
|
|||
|
|
for key in sorted(parsed.keys()):
|
|||
|
|
value = parsed[key]
|
|||
|
|
data_check_items.append(f"{key}={value}")
|
|||
|
|
data_check_string = "\n".join(data_check_items)
|
|||
|
|
|
|||
|
|
secret_key = hmac.new(
|
|||
|
|
key="WebAppData".encode("utf-8"),
|
|||
|
|
msg=bot_token.encode("utf-8"),
|
|||
|
|
digestmod=hashlib.sha256,
|
|||
|
|
).digest()
|
|||
|
|
|
|||
|
|
calculated_hash = hmac.new(
|
|||
|
|
key=secret_key,
|
|||
|
|
msg=data_check_string.encode("utf-8"),
|
|||
|
|
digestmod=hashlib.sha256,
|
|||
|
|
).hexdigest()
|
|||
|
|
|
|||
|
|
if not hmac.compare_digest(calculated_hash, received_hash):
|
|||
|
|
logger.warning("[MAX] Подпись initData не совпадает")
|
|||
|
|
raise MaxAuthError("Invalid init_data hash")
|
|||
|
|
|
|||
|
|
return parsed
|
|||
|
|
|
|||
|
|
|
|||
|
|
def extract_max_user(init_data: str) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Валидирует initData и возвращает данные пользователя MAX.
|
|||
|
|
|
|||
|
|
В поле `user` — JSON с id, first_name, last_name, username, language_code, photo_url и т.д.
|
|||
|
|
"""
|
|||
|
|
parsed = verify_max_init_data(init_data)
|
|||
|
|
|
|||
|
|
user_raw = parsed.get("user")
|
|||
|
|
if not user_raw:
|
|||
|
|
logger.warning("[MAX] В initData отсутствует поле user")
|
|||
|
|
raise MaxAuthError("No user field in init_data")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
user_obj = json.loads(user_raw)
|
|||
|
|
except Exception as e:
|
|||
|
|
raise MaxAuthError(f"Failed to parse user JSON: {e}") from e
|
|||
|
|
|
|||
|
|
if "id" not in user_obj:
|
|||
|
|
raise MaxAuthError("MAX user.id is missing")
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"max_user_id": str(user_obj.get("id")),
|
|||
|
|
"username": user_obj.get("username"),
|
|||
|
|
"first_name": user_obj.get("first_name"),
|
|||
|
|
"last_name": user_obj.get("last_name"),
|
|||
|
|
"language_code": user_obj.get("language_code"),
|
|||
|
|
"photo_url": user_obj.get("photo_url"),
|
|||
|
|
"raw": user_obj,
|
|||
|
|
}
|