### 🍪 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:
162
auth/oauth.py
162
auth/oauth.py
@@ -16,12 +16,6 @@ from orm.community import Community, CommunityAuthor, CommunityFollower
|
||||
from settings import (
|
||||
FRONTEND_URL,
|
||||
OAUTH_CLIENTS,
|
||||
SESSION_COOKIE_DOMAIN,
|
||||
SESSION_COOKIE_HTTPONLY,
|
||||
SESSION_COOKIE_MAX_AGE,
|
||||
SESSION_COOKIE_NAME,
|
||||
SESSION_COOKIE_SAMESITE,
|
||||
SESSION_COOKIE_SECURE,
|
||||
)
|
||||
from storage.db import local_session
|
||||
from storage.redis import redis
|
||||
@@ -487,65 +481,34 @@ async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse:
|
||||
if not isinstance(redirect_uri, str) or not redirect_uri:
|
||||
redirect_uri = FRONTEND_URL
|
||||
|
||||
# 🔧 Для testing.discours.io используем httpOnly cookies + простой редирект
|
||||
from urllib.parse import urlparse
|
||||
# 🎯 Стандартный OAuth flow: токен в URL для фронтенда
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
parsed_redirect = urlparse(redirect_uri)
|
||||
parsed_url = urlparse(redirect_uri)
|
||||
|
||||
# Определяем финальный URL для редиректа
|
||||
if "testing.discours.io" in parsed_redirect.netloc:
|
||||
# 💋 Для testing.discours.io используем httpOnly cookie + токен в URL для фронтенда
|
||||
from urllib.parse import quote
|
||||
|
||||
final_redirect_url = (
|
||||
f"https://testing.discours.io/oauth?access_token={session_token}&redirect_url={quote(redirect_uri)}"
|
||||
# 🌐 OAuth: токен в URL (стандартный подход)
|
||||
logger.info("🌐 OAuth: using token in URL")
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
query_params["access_token"] = [session_token]
|
||||
if state:
|
||||
query_params["state"] = [state]
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
final_redirect_url = urlunparse(
|
||||
(
|
||||
parsed_url.scheme,
|
||||
parsed_url.netloc,
|
||||
parsed_url.path,
|
||||
parsed_url.params,
|
||||
new_query,
|
||||
parsed_url.fragment,
|
||||
)
|
||||
if state:
|
||||
final_redirect_url += f"&state={state}"
|
||||
else:
|
||||
# Для других доменов используем старую логику с токеном в URL
|
||||
from urllib.parse import parse_qs, urlencode, urlunparse
|
||||
|
||||
parsed_url = urlparse(redirect_uri)
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
|
||||
# Добавляем access_token и state в URL параметры
|
||||
query_params["access_token"] = [session_token]
|
||||
if state:
|
||||
query_params["state"] = [state]
|
||||
|
||||
# Собираем новый URL с параметрами
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
final_redirect_url = urlunparse(
|
||||
(
|
||||
parsed_url.scheme,
|
||||
parsed_url.netloc,
|
||||
parsed_url.path,
|
||||
parsed_url.params,
|
||||
new_query,
|
||||
parsed_url.fragment,
|
||||
)
|
||||
)
|
||||
|
||||
# 🍪 Устанавливаем httpOnly cookie вместо токена в URL
|
||||
response = RedirectResponse(url=redirect_uri, status_code=307)
|
||||
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=session_token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE if SESSION_COOKIE_SAMESITE in ["strict", "lax", "none"] else "none",
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
path="/",
|
||||
domain=SESSION_COOKIE_DOMAIN, # ✅ Для работы с поддоменами
|
||||
)
|
||||
|
||||
logger.info(f"✅ OAuth: httpOnly cookie установлен для user_id={author.id}")
|
||||
logger.info(f"🔗 Redirect на фронтенд БЕЗ токена в URL: {redirect_uri}")
|
||||
logger.info(
|
||||
f"🍪 Cookie: {SESSION_COOKIE_NAME}, secure={SESSION_COOKIE_SECURE}, samesite={SESSION_COOKIE_SAMESITE}"
|
||||
)
|
||||
# 🔗 Редиректим с токеном в URL
|
||||
response = RedirectResponse(url=final_redirect_url, status_code=307)
|
||||
|
||||
logger.info(f"✅ OAuth: токен передан в URL для user_id={author.id}")
|
||||
logger.info(f"🔗 Redirect URL: {final_redirect_url}")
|
||||
|
||||
logger.info(f"OAuth успешно завершен для {provider}, user_id={author.id}")
|
||||
return response
|
||||
@@ -811,44 +774,28 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon
|
||||
if not isinstance(redirect_uri, str) or not redirect_uri:
|
||||
redirect_uri = FRONTEND_URL
|
||||
|
||||
# 🔧 Для testing.discours.io используем httpOnly cookies + простой редирект
|
||||
from urllib.parse import urlparse
|
||||
# 🎯 Стандартный OAuth flow: токен в URL для фронтенда
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
parsed_redirect = urlparse(redirect_uri)
|
||||
parsed_url = urlparse(redirect_uri)
|
||||
|
||||
# Определяем финальный URL для редиректа
|
||||
if "testing.discours.io" in parsed_redirect.netloc:
|
||||
# 💋 Для testing.discours.io используем httpOnly cookie + токен в URL для фронтенда
|
||||
from urllib.parse import quote
|
||||
|
||||
final_redirect_url = (
|
||||
f"https://testing.discours.io/oauth?access_token={session_token}&redirect_url={quote(redirect_uri)}"
|
||||
)
|
||||
if state:
|
||||
final_redirect_url += f"&state={state}"
|
||||
else:
|
||||
# Для других доменов используем старую логику с токеном в URL
|
||||
from urllib.parse import parse_qs, urlencode, urlunparse
|
||||
|
||||
parsed_url = urlparse(redirect_uri)
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
|
||||
# Добавляем access_token и state в URL параметры
|
||||
query_params["access_token"] = [session_token]
|
||||
# 🌐 OAuth: токен в URL (стандартный подход)
|
||||
logger.info("🌐 OAuth: using token in URL")
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
query_params["access_token"] = [session_token]
|
||||
if state:
|
||||
query_params["state"] = [state]
|
||||
|
||||
# Собираем новый URL с параметрами
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
final_redirect_url = urlunparse(
|
||||
(
|
||||
parsed_url.scheme,
|
||||
parsed_url.netloc,
|
||||
parsed_url.path,
|
||||
parsed_url.params,
|
||||
new_query,
|
||||
parsed_url.fragment,
|
||||
)
|
||||
new_query = urlencode(query_params, doseq=True)
|
||||
final_redirect_url = urlunparse(
|
||||
(
|
||||
parsed_url.scheme,
|
||||
parsed_url.netloc,
|
||||
parsed_url.path,
|
||||
parsed_url.params,
|
||||
new_query,
|
||||
parsed_url.fragment,
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(f"🔗 OAuth redirect URL: {final_redirect_url}")
|
||||
|
||||
@@ -861,30 +808,11 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon
|
||||
logger.info(f" - Provider: {provider}")
|
||||
logger.info(f" - User ID: {author.id}")
|
||||
|
||||
# 🍪 Устанавливаем httpOnly cookie вместо токена в URL
|
||||
response = RedirectResponse(url=redirect_uri, status_code=307)
|
||||
# 🔗 Редиректим с токеном в URL
|
||||
response = RedirectResponse(url=final_redirect_url, status_code=307)
|
||||
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=session_token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
path="/",
|
||||
domain=SESSION_COOKIE_DOMAIN, # ✅ Для работы с поддоменами
|
||||
)
|
||||
|
||||
logger.info(f"✅ OAuth: httpOnly cookie установлен для user_id={author.id}")
|
||||
logger.info(f"🔗 Redirect на фронтенд БЕЗ токена в URL: {redirect_uri}")
|
||||
logger.info(
|
||||
f"🍪 Cookie: {SESSION_COOKIE_NAME}, secure={SESSION_COOKIE_SECURE}, samesite={SESSION_COOKIE_SAMESITE}"
|
||||
)
|
||||
logger.info(
|
||||
f"🔍 Session token preview: {session_token[:30]}..."
|
||||
if len(session_token) > 30
|
||||
else f"🔍 Session token: {session_token}"
|
||||
)
|
||||
logger.info(f"✅ OAuth: токен передан в URL для user_id={author.id}")
|
||||
logger.info(f"🔗 Final redirect URL: {final_redirect_url}")
|
||||
|
||||
logger.info(f"✅ OAuth успешно завершен для {provider}, user_id={author.id}")
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user