chatty / app /api /auth.py
GitHub Action
Deploy to Hugging Face Space
3470cf9
Raw
History Blame Contribute Delete
3 kB
from __future__ import annotations
import hashlib
import hmac
import json
from urllib.parse import parse_qs, unquote
from fastapi import Header, HTTPException, status
from app.core.config import get_settings, is_user_allowed
_DEV_USER_ID = 377324522
def _extract_user_id(init_data: str) -> int:
"""Parse user.id from an initData query string without verifying the signature."""
parsed = parse_qs(init_data)
user_data = parsed.get("user", [""])[0]
if not user_data:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "missing user")
try:
user_obj = json.loads(unquote(user_data))
return int(user_obj["id"])
except (json.JSONDecodeError, KeyError, ValueError) as exc:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "bad user data") from exc
def _validate_init_data(init_data: str, bot_token: str) -> int:
"""Verify Telegram Mini App initData HMAC-SHA256 and return the user_id."""
parsed = parse_qs(init_data)
check_hash = parsed.get("hash", [""])[0]
if not check_hash:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "missing hash")
data_pairs = []
for key_val in init_data.split("&"):
key, _, val = key_val.partition("=")
if key != "hash":
data_pairs.append(f"{key}={unquote(val)}")
data_pairs.sort()
data_check_string = "\n".join(data_pairs)
secret_key = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
computed = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(computed, check_hash):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid hash")
return _extract_user_id(init_data)
def _is_dev_mode() -> bool:
"""True when running behind ngrok (local dev tunnel)."""
return "ngrok" in (get_settings().webhook_base_url or "")
def resolve_user_id(init_data: str | None) -> int:
"""Resolve the Telegram user_id from Mini App initData.
With initData present: verify the HMAC in production; in dev (ngrok) parse the
real user.id leniently so each Telegram user keeps a separate account instead of
all collapsing onto the dev fallback. Without initData: dev returns a fixed id,
production rejects with 401.
"""
if init_data:
if _is_dev_mode():
return _extract_user_id(init_data)
token = get_settings().telegram_bot_token.get_secret_value()
return _validate_init_data(init_data, token)
if _is_dev_mode():
return _DEV_USER_ID
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "missing initData")
async def get_current_user_id(
x_telegram_init_data: str | None = Header(default=None),
) -> int:
"""FastAPI dependency: resolve the Telegram user_id and enforce the allowlist."""
user_id = resolve_user_id(x_telegram_init_data)
if not is_user_allowed(user_id):
raise HTTPException(status.HTTP_403_FORBIDDEN, "user not allowed")
return user_id