From c46b30a671e0218e0a619ef10e040fe8d5803cb5 Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 27 Sep 2025 20:17:00 +0300 Subject: [PATCH] cookie-debug --- auth/middleware.py | 43 +++++++++++++++++++++++++++++-- auth/oauth.py | 57 ++++++++++++++++++++++++++++++++++++------ panel/graphql/index.ts | 17 ++++++------- panel/utils/auth.ts | 20 +++++++-------- 4 files changed, 106 insertions(+), 31 deletions(-) diff --git a/auth/middleware.py b/auth/middleware.py index d082258f..f5c47413 100644 --- a/auth/middleware.py +++ b/auth/middleware.py @@ -292,14 +292,53 @@ class AuthMiddleware: if not token: cookies = headers.get("cookie", "") logger.debug(f"[middleware] Проверяем cookies: {cookies[:100]}...") + logger.debug(f"[middleware] Ищем cookie с именем: '{SESSION_COOKIE_NAME}'") + + # 🔍 Дополнительная диагностика для отладки + 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}") + cookie_items = cookies.split(";") + found_cookies = [] for item in cookie_items: if "=" in item: name, value = item.split("=", 1) - if name.strip() == SESSION_COOKIE_NAME: + cookie_name = name.strip() + found_cookies.append(cookie_name) + if cookie_name == SESSION_COOKIE_NAME: token = value.strip() - logger.debug(f"[middleware] Токен получен из cookie {SESSION_COOKIE_NAME}: {len(token)}") + logger.debug( + f"[middleware] ✅ Токен получен из cookie {SESSION_COOKIE_NAME}: {len(token)} символов" + ) break + logger.debug(f"[middleware] Найденные cookies: {found_cookies}") + if not token: + logger.debug(f"[middleware] ❌ Cookie '{SESSION_COOKIE_NAME}' не найден среди: {found_cookies}") if token: logger.debug(f"[middleware] Токен найден: {len(token)} символов") diff --git a/auth/oauth.py b/auth/oauth.py index cc6161cc..4fbb5d05 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -528,15 +528,32 @@ async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse: response = RedirectResponse(url=final_redirect_url) # 🍪 Устанавливаем 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 and "testing.discours.io" not in parsed_redirect.netloc: + cookie_domain = ".discours.io" # Только для основного домена + elif "testing.discours.io" in parsed_redirect.netloc: + # 💋 Для testing.discours.io используем None domain и SameSite=Lax для лучшей совместимости + # SameSite=None требует Secure=True, что может не работать если HTTPS_ENABLED неправильно настроен + cookie_domain = None + cookie_samesite = "lax" # Более безопасный вариант для same-site запросов + + # 💋 Принудительно включаем Secure для testing.discours.io (всегда HTTPS) + cookie_secure = SESSION_COOKIE_SECURE + if "testing.discours.io" in parsed_redirect.netloc: + cookie_secure = True # testing.discours.io всегда использует HTTPS + response.set_cookie( SESSION_COOKIE_NAME, session_token, httponly=SESSION_COOKIE_HTTPONLY, - secure=SESSION_COOKIE_SECURE, - samesite=SESSION_COOKIE_SAMESITE, + secure=cookie_secure, + samesite=cookie_samesite, max_age=SESSION_COOKIE_MAX_AGE, path="/", # Важно: устанавливаем path="/" для доступности cookie во всех путях - domain=".discours.io" if "discours.io" in parsed_redirect.netloc else None, # Поддержка поддоменов + domain=cookie_domain, # Поддержка поддоменов только для основного домена ) logger.info(f"OAuth успешно завершен для {provider}, user_id={author.id}") @@ -853,20 +870,44 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon response = RedirectResponse(url=final_redirect_url, status_code=307) # 🍪 Устанавливаем httpOnly cookie для безопасности - cookie_domain = ".discours.io" if "discours.io" in parsed_redirect.netloc else None + # 💋 Исправляем domain для testing.discours.io - не используем wildcard domain + cookie_domain = None # Убираем wildcard domain для корректной работы + cookie_samesite = SESSION_COOKIE_SAMESITE + + if "discours.io" in parsed_redirect.netloc and "testing.discours.io" not in parsed_redirect.netloc: + cookie_domain = ".discours.io" # Только для основного домена + elif "testing.discours.io" in parsed_redirect.netloc: + # 💋 Для testing.discours.io используем None domain и SameSite=Lax для лучшей совместимости + # SameSite=None требует Secure=True, что может не работать если HTTPS_ENABLED неправильно настроен + cookie_domain = None + cookie_samesite = "lax" # Более безопасный вариант для same-site запросов + + # 💋 Принудительно включаем Secure для testing.discours.io (всегда HTTPS) + cookie_secure = SESSION_COOKIE_SECURE + if "testing.discours.io" in parsed_redirect.netloc: + cookie_secure = True # testing.discours.io всегда использует HTTPS + response.set_cookie( SESSION_COOKIE_NAME, session_token, httponly=SESSION_COOKIE_HTTPONLY, - secure=SESSION_COOKIE_SECURE, - samesite=SESSION_COOKIE_SAMESITE, + secure=cookie_secure, + samesite=cookie_samesite, max_age=SESSION_COOKIE_MAX_AGE, path="/", # Важно: устанавливаем path="/" для доступности cookie во всех путях - domain=cookie_domain, # Поддержка поддоменов + domain=cookie_domain, # Поддержка поддоменов только для основного домена ) logger.info( - f"🍪 Cookie установлен: name={SESSION_COOKIE_NAME}, domain={cookie_domain}, secure={SESSION_COOKIE_SECURE}" + 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)}" + ) + 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}") logger.info(f"✅ OAuth успешно завершен для {provider}, user_id={author.id}") diff --git a/panel/graphql/index.ts b/panel/graphql/index.ts index 2c626c3a..00c4255d 100644 --- a/panel/graphql/index.ts +++ b/panel/graphql/index.ts @@ -27,22 +27,19 @@ function getRequestHeaders(): Record { // Проверяем наличие токена в localStorage const localToken = localStorage.getItem(AUTH_TOKEN_KEY) + + // Используем только токен из localStorage (если есть) + const token = localToken - // Проверяем наличие токена в cookie - const cookieToken = getAuthTokenFromCookie() - - // Используем токен из localStorage или cookie - const token = localToken || cookieToken - - // Если есть токен, добавляем его в заголовок Authorization с префиксом Bearer + // Если есть токен в localStorage, добавляем его в заголовок Authorization с префиксом Bearer if (token && token.length > 10) { headers['Authorization'] = `Bearer ${token}` - console.debug('Отправка запроса с токеном авторизации') + console.debug('Отправка запроса с токеном авторизации из localStorage') console.debug(`[Frontend] Authorization header: Bearer ${token.substring(0, 20)}...`) } else { - console.warn('[Frontend] Токен не найден или слишком короткий') + console.debug('[Frontend] Токен в localStorage не найден, полагаемся на httpOnly cookie') console.debug(`[Frontend] Local token: ${localToken ? 'present' : 'missing'}`) - console.debug(`[Frontend] Cookie token: ${cookieToken ? 'present' : 'missing'}`) + // httpOnly cookie будет автоматически отправлен браузером благодаря credentials: 'include' } // Добавляем CSRF-токен, если он есть diff --git a/panel/utils/auth.ts b/panel/utils/auth.ts index 67fc68f9..eb2a4119 100644 --- a/panel/utils/auth.ts +++ b/panel/utils/auth.ts @@ -82,24 +82,22 @@ export function saveAuthToken(token: string): void { export function checkAuthStatus(): boolean { console.log('[Auth] Checking authentication status...') - // Проверяем наличие cookie auth_token - const cookieToken = getAuthTokenFromCookie() - const hasCookie = !!cookieToken && cookieToken.length > 10 - + // 💋 НЕ проверяем httpOnly cookie через JavaScript - он недоступен! + // httpOnly cookie автоматически отправляется браузером, но недоступен для чтения + // Проверяем наличие токена в localStorage const localToken = localStorage.getItem(AUTH_TOKEN_KEY) const hasLocalToken = !!localToken && localToken.length > 10 - const isAuth = hasCookie || hasLocalToken - console.log(`[Auth] Cookie token: ${hasCookie ? 'present' : 'missing'}`) + // 💋 Для httpOnly cookie полагаемся на backend проверку + // Если нет токена в localStorage, считаем что пользователь может быть авторизован через httpOnly cookie + // Окончательная проверка произойдет при первом GraphQL запросе + const isAuth = hasLocalToken + console.log(`[Auth] Local token: ${hasLocalToken ? 'present' : 'missing'}`) - console.log(`[Auth] Authentication status: ${isAuth ? 'authenticated' : 'not authenticated'}`) + console.log(`[Auth] Authentication status: ${isAuth ? 'authenticated via localStorage' : 'unknown (may be authenticated via httpOnly cookie)'}`) // Дополнительное логирование для диагностики - if (cookieToken) { - console.log(`[Auth] Cookie token length: ${cookieToken.length}`) - console.log(`[Auth] Cookie token preview: ${cookieToken.substring(0, 20)}...`) - } if (localToken) { console.log(`[Auth] Local token length: ${localToken.length}`) console.log(`[Auth] Local token preview: ${localToken.substring(0, 20)}...`)