[0.9.28] - OAuth/Auth with httpOnly cookie
All checks were successful
Deploy on push / deploy (push) Successful in 4m32s
All checks were successful
Deploy on push / deploy (push) Successful in 4m32s
This commit is contained in:
@@ -50,7 +50,7 @@ async def logout(request: Request) -> Response:
|
||||
key=SESSION_COOKIE_NAME,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
samesite=SESSION_COOKIE_SAMESITE if SESSION_COOKIE_SAMESITE in ["strict", "lax", "none"] else "none",
|
||||
)
|
||||
logger.info("[auth] logout: Cookie успешно удалена")
|
||||
|
||||
@@ -117,7 +117,7 @@ async def refresh_token(request: Request) -> JSONResponse:
|
||||
value=new_token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
samesite=SESSION_COOKIE_SAMESITE if SESSION_COOKIE_SAMESITE in ["strict", "lax", "none"] else "none",
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Единый middleware для обработки авторизации в GraphQL запросах
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from collections.abc import Awaitable, MutableMapping
|
||||
from typing import Any, Callable
|
||||
@@ -21,8 +20,8 @@ from settings import (
|
||||
ADMIN_EMAILS as ADMIN_EMAILS_LIST,
|
||||
)
|
||||
from settings import (
|
||||
SESSION_COOKIE_DOMAIN,
|
||||
SESSION_COOKIE_HTTPONLY,
|
||||
SESSION_COOKIE_MAX_AGE,
|
||||
SESSION_COOKIE_NAME,
|
||||
SESSION_COOKIE_SAMESITE,
|
||||
SESSION_COOKIE_SECURE,
|
||||
@@ -294,34 +293,12 @@ class AuthMiddleware:
|
||||
logger.debug(f"[middleware] Проверяем cookies: {cookies[:100]}...")
|
||||
logger.debug(f"[middleware] Ищем cookie с именем: '{SESSION_COOKIE_NAME}'")
|
||||
|
||||
# 🔍 Дополнительная диагностика для отладки
|
||||
# 🔍 Диагностика cookies (только для debug уровня)
|
||||
if not cookies:
|
||||
logger.warning("[middleware] 🚨 ПРОБЛЕМА: Cookie заголовок полностью отсутствует!")
|
||||
logger.warning(f"[middleware] 🔍 Все заголовки: {list(headers.keys())}")
|
||||
# Проверяем, есть ли активные сессии для этого пользователя
|
||||
try:
|
||||
session_keys = await redis_adapter.keys("session:*")
|
||||
if session_keys:
|
||||
logger.warning(
|
||||
f"[middleware] 🔍 В Redis найдено {len(session_keys)} активных сессий, но cookie не передается!"
|
||||
)
|
||||
# Показываем первые 3 сессии для диагностики
|
||||
for session_key in session_keys[:3]:
|
||||
try:
|
||||
session_data = await redis_adapter.hgetall(session_key)
|
||||
if session_data:
|
||||
user_id = (
|
||||
session_key.decode("utf-8").split(":")[1]
|
||||
if isinstance(session_key, bytes)
|
||||
else session_key.split(":")[1]
|
||||
)
|
||||
logger.warning(
|
||||
f"[middleware] 🔍 Активная сессия для user_id={user_id}: {session_key}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"[middleware] Ошибка чтения сессии {session_key}: {e}")
|
||||
except Exception as e:
|
||||
logger.debug(f"[middleware] Ошибка проверки сессий: {e}")
|
||||
logger.debug("[middleware] Cookie заголовок отсутствует")
|
||||
logger.debug(f"[middleware] Доступные заголовки: {list(headers.keys())}")
|
||||
# 💋 OAuth не использует cookies - это нормальное поведение
|
||||
logger.debug("[middleware] OAuth система работает без cookies - токены передаются через заголовки")
|
||||
|
||||
cookie_items = cookies.split(";")
|
||||
found_cookies = []
|
||||
@@ -472,23 +449,7 @@ class AuthMiddleware:
|
||||
"""
|
||||
|
||||
# Проверяем, является ли result уже объектом Response
|
||||
if isinstance(result, Response):
|
||||
response = result
|
||||
# Пытаемся получить данные из response для проверки логина/логаута
|
||||
result_data = {}
|
||||
if isinstance(result, JSONResponse):
|
||||
try:
|
||||
body_content = result.body
|
||||
if isinstance(body_content, bytes | memoryview):
|
||||
body_text = bytes(body_content).decode("utf-8")
|
||||
result_data = json.loads(body_text)
|
||||
else:
|
||||
result_data = json.loads(str(body_content))
|
||||
except Exception as e:
|
||||
logger.error(f"[process_result] Не удалось извлечь данные из JSONResponse: {e!s}")
|
||||
else:
|
||||
response = JSONResponse(result)
|
||||
result_data = result
|
||||
response = result if isinstance(result, Response) else JSONResponse(result)
|
||||
|
||||
# Проверяем, был ли токен в запросе или ответе
|
||||
if request.method == "POST":
|
||||
@@ -496,55 +457,13 @@ class AuthMiddleware:
|
||||
data = await request.json()
|
||||
op_name = data.get("operationName", "").lower()
|
||||
|
||||
# Если это операция логина или обновления токена, и в ответе есть токен
|
||||
if op_name in ["login", "refreshtoken"]:
|
||||
token = None
|
||||
# Пытаемся извлечь токен из данных ответа
|
||||
if result_data and isinstance(result_data, dict):
|
||||
data_obj = result_data.get("data", {})
|
||||
if isinstance(data_obj, dict) and op_name in data_obj:
|
||||
op_result = data_obj.get(op_name, {})
|
||||
if isinstance(op_result, dict) and "token" in op_result:
|
||||
token = op_result.get("token")
|
||||
|
||||
if token:
|
||||
# Устанавливаем cookie с токеном
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
)
|
||||
logger.debug(
|
||||
f"[graphql_handler] Установлена cookie {SESSION_COOKIE_NAME} для операции {op_name}"
|
||||
)
|
||||
|
||||
# Если это операция getSession и в ответе есть токен, устанавливаем cookie
|
||||
elif op_name == "getsession":
|
||||
token = None
|
||||
# Пытаемся извлечь токен из данных ответа
|
||||
if result_data and isinstance(result_data, dict):
|
||||
data_obj = result_data.get("data", {})
|
||||
if isinstance(data_obj, dict) and "getSession" in data_obj:
|
||||
op_result = data_obj.get("getSession", {})
|
||||
if isinstance(op_result, dict) and "token" in op_result and op_result.get("success"):
|
||||
token = op_result.get("token")
|
||||
|
||||
if token:
|
||||
# Устанавливаем cookie с токеном для поддержания сессии
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
)
|
||||
logger.debug(
|
||||
f"[graphql_handler] Установлена cookie {SESSION_COOKIE_NAME} для операции {op_name}"
|
||||
)
|
||||
# 💋 OAuth НЕ использует cookies - токены передаются только через заголовки/localStorage
|
||||
# Убираем автоматическую установку cookies для login/refreshtoken/getSession
|
||||
if op_name in ["login", "refreshtoken", "getsession"]:
|
||||
logger.debug(f"[graphql_handler] Операция {op_name}: токены передаются БЕЗ cookies")
|
||||
logger.debug(
|
||||
"[graphql_handler] Фронтенд должен извлечь токен из ответа и управлять им самостоятельно"
|
||||
)
|
||||
|
||||
# Если это операция logout, удаляем cookie
|
||||
elif op_name == "logout":
|
||||
@@ -552,7 +471,10 @@ class AuthMiddleware:
|
||||
key=SESSION_COOKIE_NAME,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
samesite=SESSION_COOKIE_SAMESITE
|
||||
if SESSION_COOKIE_SAMESITE in ["strict", "lax", "none"]
|
||||
else "none",
|
||||
domain=SESSION_COOKIE_DOMAIN, # ✅ КРИТИЧНО: тот же domain что при установке
|
||||
)
|
||||
logger.debug(f"[graphql_handler] Удалена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -16,6 +16,7 @@ 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,
|
||||
@@ -526,48 +527,25 @@ async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse:
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(f"🔗 OAuth redirect URL: {final_redirect_url}")
|
||||
|
||||
# Создаем ответ с редиректом
|
||||
response = RedirectResponse(url=final_redirect_url)
|
||||
|
||||
# 🔍 Диагностика перед установкой cookie
|
||||
logger.info(f"🔍 Готовимся установить cookie для redirect на: {parsed_redirect.netloc}")
|
||||
logger.info(f"🔍 Текущие настройки cookie: secure={SESSION_COOKIE_SECURE}, samesite={SESSION_COOKIE_SAMESITE}")
|
||||
|
||||
# 🍪 Устанавливаем httpOnly cookie для безопасности
|
||||
# 💋 Исправляем domain для testing.discours.io - не используем wildcard domain
|
||||
cookie_domain = None # Убираем wildcard domain для корректной работы
|
||||
cookie_samesite = SESSION_COOKIE_SAMESITE
|
||||
|
||||
if "discours.io" in parsed_redirect.netloc:
|
||||
# 💋 ЭКСТРЕННОЕ ИСПРАВЛЕНИЕ: Используем wildcard domain для всех discours.io
|
||||
cookie_domain = ".discours.io" # Работает для всех поддоменов включая testing.discours.io
|
||||
cookie_samesite = "lax" # Безопасный вариант для same-site запросов
|
||||
|
||||
# 💋 Принудительно включаем Secure для всех discours.io доменов (всегда HTTPS)
|
||||
cookie_secure = SESSION_COOKIE_SECURE
|
||||
if "discours.io" in parsed_redirect.netloc:
|
||||
cookie_secure = True # Все discours.io домены используют HTTPS
|
||||
# 🍪 Устанавливаем httpOnly cookie вместо токена в URL
|
||||
response = RedirectResponse(url=redirect_uri, status_code=307)
|
||||
|
||||
response.set_cookie(
|
||||
SESSION_COOKIE_NAME,
|
||||
session_token,
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=session_token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=cookie_secure,
|
||||
samesite=cookie_samesite,
|
||||
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="/", # Важно: устанавливаем path="/" для доступности cookie во всех путях
|
||||
domain=cookie_domain, # Поддержка поддоменов только для основного домена
|
||||
path="/",
|
||||
domain=SESSION_COOKIE_DOMAIN, # ✅ Для работы с поддоменами
|
||||
)
|
||||
|
||||
# 🔍 Дополнительная диагностика cookie
|
||||
logger.warning("🚨 ВАЖНО: Cookie должен быть установлен в браузере и отправляться в последующих запросах!")
|
||||
logger.warning("🚨 Если cookie не передается, проверьте:")
|
||||
logger.warning(f" - Браузер принимает cookie с domain={cookie_domain}")
|
||||
logger.warning(f" - HTTPS работает правильно (secure={cookie_secure})")
|
||||
logger.warning(f" - SameSite политика не блокирует (samesite={cookie_samesite})")
|
||||
logger.warning(" - Path='/' доступен для всех запросов")
|
||||
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"OAuth успешно завершен для {provider}, user_id={author.id}")
|
||||
return response
|
||||
@@ -883,55 +861,30 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon
|
||||
logger.info(f" - Provider: {provider}")
|
||||
logger.info(f" - User ID: {author.id}")
|
||||
|
||||
# Возвращаем redirect с токеном в URL
|
||||
response = RedirectResponse(url=final_redirect_url, status_code=307)
|
||||
|
||||
# 🍪 Устанавливаем httpOnly cookie для безопасности
|
||||
# 💋 Исправляем domain для testing.discours.io - не используем wildcard domain
|
||||
cookie_domain = None # Убираем wildcard domain для корректной работы
|
||||
cookie_samesite = SESSION_COOKIE_SAMESITE
|
||||
|
||||
if "discours.io" in parsed_redirect.netloc:
|
||||
# 💋 ЭКСТРЕННОЕ ИСПРАВЛЕНИЕ: Используем wildcard domain для всех discours.io
|
||||
cookie_domain = ".discours.io" # Работает для всех поддоменов включая testing.discours.io
|
||||
cookie_samesite = "lax" # Безопасный вариант для same-site запросов
|
||||
|
||||
# 💋 Принудительно включаем Secure для всех discours.io доменов (всегда HTTPS)
|
||||
cookie_secure = SESSION_COOKIE_SECURE
|
||||
if "discours.io" in parsed_redirect.netloc:
|
||||
cookie_secure = True # Все discours.io домены используют HTTPS
|
||||
# 🍪 Устанавливаем httpOnly cookie вместо токена в URL
|
||||
response = RedirectResponse(url=redirect_uri, status_code=307)
|
||||
|
||||
response.set_cookie(
|
||||
SESSION_COOKIE_NAME,
|
||||
session_token,
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=session_token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=cookie_secure,
|
||||
samesite=cookie_samesite,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
path="/", # Важно: устанавливаем path="/" для доступности cookie во всех путях
|
||||
domain=cookie_domain, # Поддержка поддоменов только для основного домена
|
||||
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 установлен: name={SESSION_COOKIE_NAME}, domain={cookie_domain}, secure={cookie_secure}, samesite={cookie_samesite}"
|
||||
)
|
||||
logger.info(
|
||||
f"🔍 Cookie debug: redirect_netloc={parsed_redirect.netloc}, is_testing={('testing.discours.io' in parsed_redirect.netloc)}"
|
||||
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"🔗 Final redirect: {final_redirect_url}")
|
||||
|
||||
# 🔍 Дополнительная диагностика cookie
|
||||
logger.warning("🚨 ВАЖНО: Cookie должен быть установлен в браузере и отправляться в последующих запросах!")
|
||||
logger.warning("🚨 Если cookie не передается, проверьте:")
|
||||
logger.warning(f" - Браузер принимает cookie с domain={cookie_domain}")
|
||||
logger.warning(f" - HTTPS работает правильно (secure={cookie_secure})")
|
||||
logger.warning(f" - SameSite политика не блокирует (samesite={cookie_samesite})")
|
||||
logger.warning(" - Path='/' доступен для всех запросов")
|
||||
|
||||
logger.info(f"✅ OAuth успешно завершен для {provider}, user_id={author.id}")
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user