This commit is contained in:
parent
c48f5f9368
commit
52bf78320b
|
@ -82,69 +82,91 @@ class AuthMiddleware:
|
|||
async def authenticate_user(self, token: str) -> tuple[AuthCredentials, AuthenticatedUser | UnauthenticatedUser]:
|
||||
"""Аутентифицирует пользователя по токену"""
|
||||
if not token:
|
||||
logger.debug("[auth.authenticate] Токен отсутствует")
|
||||
return AuthCredentials(
|
||||
author_id=None, scopes={}, logged_in=False, error_message="no token", email=None, token=None
|
||||
), UnauthenticatedUser()
|
||||
|
||||
# Проверяем сессию в Redis
|
||||
payload = await TokenManager.verify_session(token)
|
||||
if not payload:
|
||||
logger.debug("[auth.authenticate] Недействительный токен")
|
||||
return AuthCredentials(
|
||||
author_id=None, scopes={}, logged_in=False, error_message="Invalid token", email=None, token=None
|
||||
), UnauthenticatedUser()
|
||||
try:
|
||||
payload = await TokenManager.verify_session(token)
|
||||
if not payload:
|
||||
logger.debug("[auth.authenticate] Недействительный токен или сессия не найдена")
|
||||
return AuthCredentials(
|
||||
author_id=None,
|
||||
scopes={},
|
||||
logged_in=False,
|
||||
error_message="Invalid token or session",
|
||||
email=None,
|
||||
token=None,
|
||||
), UnauthenticatedUser()
|
||||
|
||||
with local_session() as session:
|
||||
try:
|
||||
author = session.query(Author).filter(Author.id == payload.user_id).one()
|
||||
with local_session() as session:
|
||||
try:
|
||||
author = session.query(Author).filter(Author.id == payload.user_id).one()
|
||||
|
||||
if author.is_locked():
|
||||
logger.debug(f"[auth.authenticate] Аккаунт заблокирован: {author.id}")
|
||||
if author.is_locked():
|
||||
logger.debug(f"[auth.authenticate] Аккаунт заблокирован: {author.id}")
|
||||
return AuthCredentials(
|
||||
author_id=None,
|
||||
scopes={},
|
||||
logged_in=False,
|
||||
error_message="Account is locked",
|
||||
email=None,
|
||||
token=None,
|
||||
), UnauthenticatedUser()
|
||||
|
||||
# Получаем разрешения из ролей
|
||||
scopes = author.get_permissions()
|
||||
|
||||
# Получаем роли для пользователя
|
||||
roles = [role.id for role in author.roles] if author.roles else []
|
||||
|
||||
# Обновляем last_seen
|
||||
author.last_seen = int(time.time())
|
||||
session.commit()
|
||||
|
||||
# Создаем объекты авторизации с сохранением токена
|
||||
credentials = AuthCredentials(
|
||||
author_id=author.id,
|
||||
scopes=scopes,
|
||||
logged_in=True,
|
||||
error_message="",
|
||||
email=author.email,
|
||||
token=token,
|
||||
)
|
||||
|
||||
user = AuthenticatedUser(
|
||||
user_id=str(author.id),
|
||||
username=author.slug or author.email or "",
|
||||
roles=roles,
|
||||
permissions=scopes,
|
||||
token=token,
|
||||
)
|
||||
|
||||
logger.debug(f"[auth.authenticate] Успешная аутентификация: {author.email}")
|
||||
return credentials, user
|
||||
|
||||
except exc.NoResultFound:
|
||||
logger.debug("[auth.authenticate] Пользователь не найден в базе данных")
|
||||
return AuthCredentials(
|
||||
author_id=None,
|
||||
scopes={},
|
||||
logged_in=False,
|
||||
error_message="Account is locked",
|
||||
error_message="User not found",
|
||||
email=None,
|
||||
token=None,
|
||||
), UnauthenticatedUser()
|
||||
|
||||
# Получаем разрешения из ролей
|
||||
scopes = author.get_permissions()
|
||||
|
||||
# Получаем роли для пользователя
|
||||
roles = [role.id for role in author.roles] if author.roles else []
|
||||
|
||||
# Обновляем last_seen
|
||||
author.last_seen = int(time.time())
|
||||
session.commit()
|
||||
|
||||
# Создаем объекты авторизации с сохранением токена
|
||||
credentials = AuthCredentials(
|
||||
author_id=author.id,
|
||||
scopes=scopes,
|
||||
logged_in=True,
|
||||
error_message="",
|
||||
email=author.email,
|
||||
token=token,
|
||||
)
|
||||
|
||||
user = AuthenticatedUser(
|
||||
user_id=str(author.id),
|
||||
username=author.slug or author.email or "",
|
||||
roles=roles,
|
||||
permissions=scopes,
|
||||
token=token,
|
||||
)
|
||||
|
||||
logger.debug(f"[auth.authenticate] Успешная аутентификация: {author.email}")
|
||||
return credentials, user
|
||||
|
||||
except exc.NoResultFound:
|
||||
logger.debug("[auth.authenticate] Пользователь не найден")
|
||||
return AuthCredentials(
|
||||
author_id=None, scopes={}, logged_in=False, error_message="User not found", email=None, token=None
|
||||
), UnauthenticatedUser()
|
||||
except Exception as e:
|
||||
logger.error(f"[auth.authenticate] Ошибка при работе с базой данных: {e}")
|
||||
return AuthCredentials(
|
||||
author_id=None, scopes={}, logged_in=False, error_message=str(e), email=None, token=None
|
||||
), UnauthenticatedUser()
|
||||
except Exception as e:
|
||||
logger.error(f"[auth.authenticate] Ошибка при проверке сессии: {e}")
|
||||
return AuthCredentials(
|
||||
author_id=None, scopes={}, logged_in=False, error_message=str(e), email=None, token=None
|
||||
), UnauthenticatedUser()
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
|
|
|
@ -15,7 +15,15 @@ from auth.tokens.storage import TokenStorage
|
|||
from resolvers.auth import generate_unique_slug
|
||||
from services.db import local_session
|
||||
from services.redis import redis
|
||||
from settings import FRONTEND_URL, OAUTH_CLIENTS
|
||||
from settings import (
|
||||
FRONTEND_URL,
|
||||
OAUTH_CLIENTS,
|
||||
SESSION_COOKIE_HTTPONLY,
|
||||
SESSION_COOKIE_MAX_AGE,
|
||||
SESSION_COOKIE_NAME,
|
||||
SESSION_COOKIE_SAMESITE,
|
||||
SESSION_COOKIE_SECURE,
|
||||
)
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
# Type для dependency injection сессии
|
||||
|
@ -302,7 +310,10 @@ async def oauth_login(_: None, _info: GraphQLResolveInfo, provider: str, callbac
|
|||
|
||||
|
||||
async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse:
|
||||
"""Обрабатывает callback от OAuth провайдера"""
|
||||
"""
|
||||
Обработчик OAuth callback.
|
||||
Создает или обновляет пользователя и устанавливает сессионный токен.
|
||||
"""
|
||||
try:
|
||||
# Получаем state из query параметров
|
||||
state = request.query_params.get("state")
|
||||
|
@ -341,12 +352,12 @@ async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse:
|
|||
redirect_url = f"{stored_redirect_uri}?state={state}&access_token={session_token}"
|
||||
response = RedirectResponse(url=redirect_url)
|
||||
response.set_cookie(
|
||||
"session_token",
|
||||
SESSION_COOKIE_NAME,
|
||||
session_token,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="lax",
|
||||
max_age=30 * 24 * 60 * 60, # 30 days
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
)
|
||||
return response
|
||||
|
||||
|
@ -460,12 +471,12 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon
|
|||
# Возвращаем redirect с cookie
|
||||
response = RedirectResponse(url="/auth/success", status_code=307)
|
||||
response.set_cookie(
|
||||
"session_token",
|
||||
SESSION_COOKIE_NAME,
|
||||
session_token,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="lax",
|
||||
max_age=30 * 24 * 60 * 60, # 30 дней
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
)
|
||||
return response
|
||||
|
||||
|
|
|
@ -230,6 +230,10 @@ class SessionTokenManager(BaseTokenManager):
|
|||
"""
|
||||
Проверяет сессию по токену для совместимости с TokenStorage
|
||||
"""
|
||||
if not token:
|
||||
logger.debug("Пустой токен")
|
||||
return None
|
||||
|
||||
logger.debug(f"Проверка сессии для токена: {token[:20]}...")
|
||||
|
||||
# Декодируем токен для получения payload
|
||||
|
@ -239,15 +243,23 @@ class SessionTokenManager(BaseTokenManager):
|
|||
logger.error("Не удалось декодировать токен")
|
||||
return None
|
||||
|
||||
if not hasattr(payload, "user_id"):
|
||||
logger.error("В токене отсутствует user_id")
|
||||
return None
|
||||
|
||||
logger.debug(f"Успешно декодирован токен, user_id={payload.user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при декодировании токена: {e}")
|
||||
return None
|
||||
|
||||
# Проверяем валидность токена
|
||||
valid, _ = await self.validate_session_token(token)
|
||||
if valid:
|
||||
logger.debug(f"Сессия найдена для пользователя {payload.user_id}")
|
||||
return payload
|
||||
logger.warning(f"Сессия не найдена: {payload.user_id}")
|
||||
return None
|
||||
try:
|
||||
valid, error = await self.validate_session_token(token)
|
||||
if valid:
|
||||
logger.debug(f"Сессия найдена для пользователя {payload.user_id}")
|
||||
return payload
|
||||
logger.warning(f"Сессия не найдена: {payload.user_id}, ошибка: {error}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при валидации сессии: {e}")
|
||||
return None
|
||||
|
|
33
main.py
33
main.py
|
@ -38,28 +38,29 @@ schema = make_executable_schema(load_schema_from_path("schema/"), list(resolvers
|
|||
|
||||
# Создаем middleware с правильным порядком
|
||||
middleware = [
|
||||
# Начинаем с обработки ошибок
|
||||
Middleware(ExceptionHandlerMiddleware),
|
||||
# CORS должен быть перед другими middleware для корректной обработки preflight-запросов
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"https://localhost:3000",
|
||||
"http://localhost:3000",
|
||||
"https://testing.discours.io",
|
||||
"https://testing.dscrs.site",
|
||||
"https://testing3.discours.io",
|
||||
"https://v3.dscrs.site",
|
||||
"https://session-daily.vercel.app",
|
||||
"https://coretest.discours.io",
|
||||
"https://core.discours.io",
|
||||
"https://discours.io",
|
||||
"https://new.discours.io",
|
||||
],
|
||||
allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS
|
||||
allow_headers=["*"],
|
||||
allow_credentials=True,
|
||||
),
|
||||
# извлечение токена + аутентификация + cookies
|
||||
# Аутентификация должна быть после CORS
|
||||
Middleware(AuthMiddleware),
|
||||
]
|
||||
|
||||
|
||||
# Создаем экземпляр GraphQL с улучшенным обработчиком
|
||||
graphql_app = GraphQL(schema, debug=DEVMODE, http_handler=EnhancedGraphQLHTTPHandler())
|
||||
|
||||
|
@ -224,26 +225,6 @@ async def lifespan(app: Starlette):
|
|||
print("[lifespan] Shutdown complete")
|
||||
|
||||
|
||||
middleware = [
|
||||
# Начинаем с обработки ошибок
|
||||
Middleware(ExceptionHandlerMiddleware),
|
||||
# CORS должен быть перед другими middleware для корректной обработки preflight-запросов
|
||||
Middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"https://localhost:3000",
|
||||
"http://localhost:3000",
|
||||
"https://testing.discours.io",
|
||||
"https://testing3.discours.io",
|
||||
"https://coretest.discours.io",
|
||||
"https://session-daily.vercel.app",
|
||||
],
|
||||
allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS
|
||||
allow_headers=["*"],
|
||||
allow_credentials=True,
|
||||
),
|
||||
]
|
||||
|
||||
# Обновляем маршрут в Starlette
|
||||
app = Starlette(
|
||||
routes=[
|
||||
|
@ -253,7 +234,7 @@ app = Starlette(
|
|||
Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]),
|
||||
Mount("/", app=StaticFiles(directory=str(DIST_DIR), html=True)),
|
||||
],
|
||||
middleware=middleware,
|
||||
middleware=middleware, # Используем единый список middleware
|
||||
lifespan=lifespan,
|
||||
debug=True,
|
||||
)
|
||||
|
|
|
@ -63,7 +63,7 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS = 30
|
|||
|
||||
# Настройки для HTTP cookies (используется в auth middleware)
|
||||
SESSION_COOKIE_NAME = "auth_token"
|
||||
SESSION_COOKIE_SECURE = False
|
||||
SESSION_COOKIE_SECURE = True # Включаем для HTTPS
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE: Literal["lax", "strict", "none"] = "lax"
|
||||
SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 дней
|
||||
|
|
Loading…
Reference in New Issue
Block a user