""" 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, }