[0.9.28] - 2025-09-28
All checks were successful
Deploy on push / deploy (push) Successful in 2m46s

### 🍪 CRITICAL Cross-Origin Auth
- **🔧 SESSION_COOKIE_DOMAIN**: Добавлена поддержка поддоменов `.discours.io` для cross-origin cookies
- **🌐 Cross-Origin SSE**: Исправлена работа Server-Sent Events с httpOnly cookies между поддоменами
- **🔐 Unified Auth**: Унифицированы настройки cookies для OAuth, login, refresh, logout операций
- **📝 MyPy Compliance**: Исправлена типизация `SESSION_COOKIE_SAMESITE` с использованием `cast()`

### 🛠️ Technical Changes
- **settings.py**: Добавлен `SESSION_COOKIE_DOMAIN` с типобезопасной настройкой SameSite
- **auth/oauth.py**: Обновлены все `set_cookie` вызовы с `domain` параметром
- **auth/middleware.py**: Добавлена поддержка `SESSION_COOKIE_DOMAIN` в logout операциях
- **resolvers/auth.py**: Унифицированы cookie настройки в login/refresh/logout resolvers
- **auth/__init__.py**: Обновлены cookie операции с domain поддержкой

### 📚 Documentation
- **docs/auth/sse-httponly-integration.md**: Новая документация по SSE + httpOnly cookies интеграции
- **docs/auth/architecture.md**: Обновлены диаграммы для unified httpOnly cookie архитектуры

### 🎯 Impact
-  **GraphQL API** (`v3.discours.io`) теперь работает с httpOnly cookies cross-origin
-  **SSE сервер** (`connect.discours.io`) работает с теми же cookies
-  **Безопасность**: httpOnly cookies защищают от XSS атак
-  **UX**: Автоматическая аутентификация без управления токенами в JavaScript
This commit is contained in:
2025-09-28 13:06:03 +03:00
parent fb98a1c6c8
commit 752e2dcbdc
9 changed files with 255 additions and 223 deletions

View File

@@ -91,8 +91,18 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A
result = await auth_service.login(email, password, request)
# Устанавливаем httpOnly cookie если есть токен
if result.get("success") and result.get("token"):
# 🎯 Проверяем откуда пришел запрос - админка или основной сайт
request = info.context.get("request")
is_admin_request = False
if request:
# Проверяем путь запроса или Referer header
referer = request.headers.get("referer", "")
origin = request.headers.get("origin", "")
is_admin_request = "/panel" in referer or "/panel" in origin or "admin" in referer
# Устанавливаем httpOnly cookie только для админки
if result.get("success") and result.get("token") and is_admin_request:
try:
response = info.context.get("response")
if not response:
@@ -109,21 +119,27 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A
else "none",
max_age=SESSION_COOKIE_MAX_AGE,
path="/",
domain=SESSION_COOKIE_DOMAIN, # ✅ КРИТИЧНО для поддоменов
domain=SESSION_COOKIE_DOMAIN,
)
logger.info(
f"Email/Password: httpOnly cookie установлен для пользователя {result.get('author', {}).get('id')}"
f"Admin login: httpOnly cookie установлен для пользователя {result.get('author', {}).get('id')}"
)
# 💋 НЕ возвращаем токен клиенту - он в httpOnly cookie
# Для админки НЕ возвращаем токен клиенту - он в httpOnly cookie
result_without_token = result.copy()
result_without_token["token"] = None # Скрываем токен от JavaScript
result_without_token["token"] = None
return result_without_token
except Exception as cookie_error:
logger.warning(f"Не удалось установить cookie: {cookie_error}")
# Для основного сайта возвращаем токен как обычно (Bearer в localStorage)
if not is_admin_request:
logger.info(
f"✅ Main site login: токен возвращен для localStorage пользователя {result.get('author', {}).get('id')}"
)
return result
except Exception as e:
logger.warning(f"Ошибка входа: {e}")