Implement phone number normalization and access control for Telegram users
- Added functionality to normalize phone numbers for comparison, ensuring only digits are stored and checked. - Updated configuration to include optional phone number allowlists for users and admins in the environment settings. - Enhanced authentication logic to allow access based on normalized phone numbers, in addition to usernames. - Introduced new helper functions for parsing and validating phone numbers, improving code organization and maintainability. - Added unit tests to validate phone normalization and access control based on phone numbers.
This commit is contained in:
@@ -16,7 +16,7 @@ def validate_init_data(
|
||||
max_age_seconds: int | None = None,
|
||||
) -> str | None:
|
||||
"""Validate initData and return username; see validate_init_data_with_reason for failure reason."""
|
||||
username, _, _ = validate_init_data_with_reason(
|
||||
_, username, _, _ = validate_init_data_with_reason(
|
||||
init_data, bot_token, max_age_seconds
|
||||
)
|
||||
return username
|
||||
@@ -34,14 +34,16 @@ def validate_init_data_with_reason(
|
||||
init_data: str,
|
||||
bot_token: str,
|
||||
max_age_seconds: int | None = None,
|
||||
) -> tuple[str | None, str, str]:
|
||||
) -> tuple[int | None, str | None, str, str]:
|
||||
"""
|
||||
Validate initData signature and return (username, reason, lang) or (None, reason, lang).
|
||||
reason is one of: "ok", "empty", "no_hash", "hash_mismatch", "auth_date_expired", "no_user", "user_invalid", "no_username".
|
||||
Validate initData signature and return (telegram_user_id, username, reason, lang).
|
||||
reason is one of: "ok", "empty", "no_hash", "hash_mismatch", "auth_date_expired",
|
||||
"no_user", "user_invalid", "no_user_id", "no_username" (legacy; no_user_id used when user.id missing).
|
||||
lang is from user.language_code normalized to 'ru' or 'en'; 'en' when no user.
|
||||
On success: (user.id, user.get('username') or None, "ok", lang) — username may be None.
|
||||
"""
|
||||
if not init_data or not bot_token:
|
||||
return (None, "empty", "en")
|
||||
return (None, None, "empty", "en")
|
||||
init_data = init_data.strip()
|
||||
params = {}
|
||||
for part in init_data.split("&"):
|
||||
@@ -53,7 +55,7 @@ def validate_init_data_with_reason(
|
||||
params[key] = value
|
||||
hash_val = params.pop("hash", None)
|
||||
if not hash_val:
|
||||
return (None, "no_hash", "en")
|
||||
return (None, None, "no_hash", "en")
|
||||
data_pairs = sorted(params.items())
|
||||
# Data-check string: key=value with URL-decoded values (per Telegram example)
|
||||
data_string = "\n".join(f"{k}={unquote(v)}" for k, v in data_pairs)
|
||||
@@ -69,28 +71,37 @@ def validate_init_data_with_reason(
|
||||
digestmod=hashlib.sha256,
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(computed.lower(), hash_val.lower()):
|
||||
return (None, "hash_mismatch", "en")
|
||||
return (None, None, "hash_mismatch", "en")
|
||||
if max_age_seconds is not None and max_age_seconds > 0:
|
||||
auth_date_raw = params.get("auth_date")
|
||||
if not auth_date_raw:
|
||||
return (None, "auth_date_expired", "en")
|
||||
return (None, None, "auth_date_expired", "en")
|
||||
try:
|
||||
auth_date = int(float(auth_date_raw))
|
||||
except (ValueError, TypeError):
|
||||
return (None, "auth_date_expired", "en")
|
||||
return (None, None, "auth_date_expired", "en")
|
||||
if time.time() - auth_date > max_age_seconds:
|
||||
return (None, "auth_date_expired", "en")
|
||||
return (None, None, "auth_date_expired", "en")
|
||||
user_raw = params.get("user")
|
||||
if not user_raw:
|
||||
return (None, "no_user", "en")
|
||||
return (None, None, "no_user", "en")
|
||||
try:
|
||||
user = json.loads(unquote(user_raw))
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return (None, "user_invalid", "en")
|
||||
return (None, None, "user_invalid", "en")
|
||||
if not isinstance(user, dict):
|
||||
return (None, "user_invalid", "en")
|
||||
return (None, None, "user_invalid", "en")
|
||||
lang = _normalize_lang(user.get("language_code"))
|
||||
raw_id = user.get("id")
|
||||
if raw_id is None:
|
||||
return (None, None, "no_user_id", lang)
|
||||
try:
|
||||
telegram_user_id = int(raw_id)
|
||||
except (TypeError, ValueError):
|
||||
return (None, None, "no_user_id", lang)
|
||||
username = user.get("username")
|
||||
if not username or not isinstance(username, str):
|
||||
return (None, "no_username", lang)
|
||||
return (username.strip().lstrip("@").lower(), "ok", lang)
|
||||
if username and isinstance(username, str):
|
||||
username = username.strip().lstrip("@").lower()
|
||||
else:
|
||||
username = None
|
||||
return (telegram_user_id, username, "ok", lang)
|
||||
|
||||
Reference in New Issue
Block a user