Files
remnawave-bedolaga-telegram…/app/cabinet/auth/telegram_auth.py
c0mrade 9a2aea038a chore: add uv package manager and ruff linter configuration
- Add pyproject.toml with uv and ruff configuration
- Pin Python version to 3.13 via .python-version
- Add Makefile commands: lint, format, fix
- Apply ruff formatting to entire codebase
- Remove unused imports (base64 in yookassa/simple_subscription)
- Update .gitignore for new config files
2026-01-24 17:45:27 +03:00

128 lines
4.2 KiB
Python

"""Telegram authentication validation for cabinet."""
import hashlib
import hmac
import json
from datetime import datetime
from typing import Any
from urllib.parse import parse_qsl, unquote
from app.config import settings
def validate_telegram_login_widget(data: dict[str, Any], max_age_seconds: int = 86400) -> bool:
"""
Validate Telegram Login Widget data.
https://core.telegram.org/widgets/login#checking-authorization
Args:
data: Dictionary with Telegram login data (id, first_name, auth_date, hash, etc.)
max_age_seconds: Maximum allowed age of auth_date (default 24 hours)
Returns:
True if data is valid, False otherwise
"""
auth_data = data.copy()
check_hash = auth_data.pop('hash', None)
if not check_hash:
return False
# Check auth_date is not too old
auth_date = auth_data.get('auth_date')
if auth_date:
try:
# Use UTC timestamp to avoid timezone issues
auth_time = datetime.utcfromtimestamp(int(auth_date))
age = (datetime.utcnow() - auth_time).total_seconds()
if age > max_age_seconds:
return False
except (ValueError, TypeError, OSError):
return False
# Build data-check-string (sorted key=value pairs, newline-separated)
data_check_arr = [f'{k}={v}' for k, v in sorted(auth_data.items()) if v is not None]
data_check_string = '\n'.join(data_check_arr)
# Create secret key from bot token using SHA256
bot_token = settings.BOT_TOKEN
secret_key = hashlib.sha256(bot_token.encode()).digest()
# Calculate expected hash
calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(calculated_hash, check_hash)
def validate_telegram_init_data(init_data: str, max_age_seconds: int = 86400) -> dict[str, Any] | None:
"""
Validate Telegram WebApp initData.
https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
Args:
init_data: Raw initData string from Telegram WebApp
max_age_seconds: Maximum allowed age of auth_date (default 24 hours)
Returns:
Parsed user data dict if valid, None otherwise
"""
try:
# Parse the init_data string
parsed = dict(parse_qsl(init_data, keep_blank_values=True))
received_hash = parsed.pop('hash', None)
if not received_hash:
return None
# Check auth_date is not too old
auth_date = parsed.get('auth_date')
if auth_date:
try:
# Use UTC timestamp to avoid timezone issues
auth_time = datetime.utcfromtimestamp(int(auth_date))
age = (datetime.utcnow() - auth_time).total_seconds()
if age > max_age_seconds:
return None
except (ValueError, TypeError, OSError):
return None
# Build data-check-string
data_check_arr = [f'{k}={v}' for k, v in sorted(parsed.items())]
data_check_string = '\n'.join(data_check_arr)
# Create secret key: HMAC_SHA256(bot_token, "WebAppData")
bot_token = settings.BOT_TOKEN
secret_key = hmac.new(b'WebAppData', bot_token.encode(), hashlib.sha256).digest()
# Calculate expected hash
calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(calculated_hash, received_hash):
return None
# Parse user data from the validated data
user_data_str = parsed.get('user')
if user_data_str:
user_data = json.loads(unquote(user_data_str))
return user_data
return parsed
except (ValueError, TypeError, json.JSONDecodeError):
return None
def extract_telegram_user_from_init_data(init_data: str) -> dict[str, Any] | None:
"""
Extract and validate user info from Telegram WebApp initData.
Args:
init_data: Raw initData string from Telegram WebApp
Returns:
User data dict with id, first_name, last_name, username, etc. or None if invalid
"""
return validate_telegram_init_data(init_data)