Refactor configuration and enhance Telegram initData validation

- Improved formatting and readability in config.py and other files by adding line breaks.
- Introduced INIT_DATA_MAX_AGE_SECONDS to enforce replay protection for Telegram initData.
- Updated validate_init_data function to include max_age_seconds parameter for validation.
- Enhanced API to reject old initData based on the new max_age_seconds setting.
- Added tests for auth_date expiry and validation of initData in test_telegram_auth.py.
- Updated README with details on the new INIT_DATA_MAX_AGE_SECONDS configuration.
This commit is contained in:
2026-02-17 17:31:20 +03:00
parent d20a285f09
commit 1948618394
19 changed files with 181 additions and 25 deletions

View File

@@ -1,4 +1,5 @@
"""FastAPI app: /api/duties and static webapp."""
import logging
import re
from pathlib import Path
@@ -50,7 +51,15 @@ def _fetch_duties_response(from_date: str, to_date: str) -> list[DutyWithUser]:
def _is_private_client(client_host: str | None) -> bool:
"""True if client is localhost or private LAN (dev / same-machine access)."""
"""True if client is localhost or private LAN (dev / same-machine access).
Note: Behind a reverse proxy (e.g. nginx, Caddy), request.client.host is often
the proxy address (e.g. 127.0.0.1). Then "private client" would be true for all
requests when initData is missing. For production, either rely on the Mini App
always sending initData, or configure the proxy to forward the real client IP
(e.g. X-Forwarded-For) and use that for this check. Do not rely on the private-IP
bypass when deployed behind a proxy without one of these measures.
"""
if not client_host:
return False
if client_host in ("127.0.0.1", "::1"):
@@ -84,19 +93,28 @@ def list_duties(
x_telegram_init_data: str | None = Header(None, alias="X-Telegram-Init-Data"),
) -> list[DutyWithUser]:
_validate_duty_dates(from_date, to_date)
log.info("GET /api/duties from %s, has initData: %s", request.client.host if request.client else "?", bool((x_telegram_init_data or "").strip()))
log.info(
"GET /api/duties from %s, has initData: %s",
request.client.host if request.client else "?",
bool((x_telegram_init_data or "").strip()),
)
init_data = (x_telegram_init_data or "").strip()
if not init_data:
client_host = request.client.host if request.client else None
if _is_private_client(client_host) or config.MINI_APP_SKIP_AUTH:
if config.MINI_APP_SKIP_AUTH:
log.warning("duties: allowing without initData (MINI_APP_SKIP_AUTH is set)")
log.warning(
"duties: allowing without initData (MINI_APP_SKIP_AUTH is set)"
)
return _fetch_duties_response(from_date, to_date)
log.warning("duties: no X-Telegram-Init-Data header (client=%s)", client_host)
raise HTTPException(status_code=403, detail="Откройте календарь из Telegram")
username = validate_init_data(init_data, config.BOT_TOKEN)
max_age = config.INIT_DATA_MAX_AGE_SECONDS or None
username = validate_init_data(init_data, config.BOT_TOKEN, max_age_seconds=max_age)
if username is None:
log.warning("duties: initData validation failed (invalid signature or no username)")
log.warning(
"duties: initData validation failed (invalid signature or no username)"
)
raise HTTPException(status_code=403, detail="Неверные данные авторизации")
if not config.can_access_miniapp(username):
log.warning("duties: username not in allowlist")