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:
28
api/app.py
28
api/app.py
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user