diff --git a/app/cabinet/auth/telegram_auth.py b/app/cabinet/auth/telegram_auth.py index f4de0e68..716c7ddb 100644 --- a/app/cabinet/auth/telegram_auth.py +++ b/app/cabinet/auth/telegram_auth.py @@ -49,7 +49,17 @@ def validate_telegram_login_widget(data: dict[str, Any], max_age_seconds: int = auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC) age = (datetime.now(UTC) - auth_time).total_seconds() if age > max_age_seconds or age < -_MAX_CLOCK_SKEW_SECONDS: + logger.warning( + 'Telegram widget auth rejected: too old', + age_hours=round(age / 3600, 1), + max_age_hours=round(max_age_seconds / 3600, 1), + ) return False + if age > 86400: + logger.info( + 'Telegram widget auth accepted with stale auth_date', + age_hours=round(age / 3600, 1), + ) except (ValueError, TypeError, OSError): return False @@ -96,7 +106,17 @@ def validate_telegram_init_data(init_data: str, max_age_seconds: int = 86400) -> auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC) age = (datetime.now(UTC) - auth_time).total_seconds() if age > max_age_seconds or age < -_MAX_CLOCK_SKEW_SECONDS: + logger.warning( + 'Telegram initData rejected: too old', + age_hours=round(age / 3600, 1), + max_age_hours=round(max_age_seconds / 3600, 1), + ) return None + if age > 86400: + logger.info( + 'Telegram initData accepted with stale auth_date (Telegram caching bug)', + age_hours=round(age / 3600, 1), + ) except (ValueError, TypeError, OSError): return None diff --git a/app/cabinet/routes/account_linking.py b/app/cabinet/routes/account_linking.py index 4ed3c8fe..3424161f 100644 --- a/app/cabinet/routes/account_linking.py +++ b/app/cabinet/routes/account_linking.py @@ -487,7 +487,8 @@ async def link_telegram( if request.init_data: # Mini App flow: validate initData - user_data = validate_telegram_init_data(request.init_data) + # Generous max_age: Telegram Desktop/iOS cache initData with stale auth_date + user_data = validate_telegram_init_data(request.init_data, max_age_seconds=86400 * 30) if not user_data or not user_data.get('id'): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -560,7 +561,8 @@ async def link_telegram( if request.photo_url is not None: widget_data['photo_url'] = request.photo_url - if not validate_telegram_login_widget(widget_data): + # Generous max_age: Telegram caches auth data with stale auth_date + if not validate_telegram_login_widget(widget_data, max_age_seconds=86400 * 30): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid or expired Telegram Login Widget data', diff --git a/app/cabinet/routes/auth.py b/app/cabinet/routes/auth.py index a69ba16c..85e5ab22 100644 --- a/app/cabinet/routes/auth.py +++ b/app/cabinet/routes/auth.py @@ -431,7 +431,11 @@ async def auth_telegram( detail='Too many requests', headers={'Retry-After': '60'}, ) - user_data = validate_telegram_init_data(request.init_data) + # Telegram Desktop/iOS cache initData with stale auth_date (known Telegram bug: + # https://github.com/telegramdesktop/tdesktop/issues/28303). + # Use generous max_age: HMAC signature proves authenticity, + # JWT tokens handle actual session expiration after login. + user_data = validate_telegram_init_data(request.init_data, max_age_seconds=86400 * 30) if not user_data: raise HTTPException( @@ -550,7 +554,8 @@ async def auth_telegram_widget( widget_data = request.model_dump(exclude={'campaign_slug', 'referral_code'}) - if not validate_telegram_login_widget(widget_data): + # Generous max_age: Telegram caches auth data with stale auth_date + if not validate_telegram_login_widget(widget_data, max_age_seconds=86400 * 30): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid or expired Telegram authentication data',