diff --git a/app/core/auth.py b/app/core/auth.py index 13d547a..197b0b0 100644 --- a/app/core/auth.py +++ b/app/core/auth.py @@ -108,6 +108,12 @@ async def get_session_data(redis: aioredis.Redis, session_id: str) -> dict[str, return json.loads(raw) +async def refresh_session_ttl(redis: aioredis.Redis, session_id: str) -> None: + """Extend the Redis session TTL on activity (sliding session).""" + settings = get_settings() + await redis.expire(f"session:{session_id}", settings.session_ttl_seconds) + + async def invalidate_session(redis: aioredis.Redis, session_id: str) -> None: """Delete a session from Redis (logout). PostgreSQL record persists.""" await redis.delete(f"session:{session_id}") diff --git a/app/core/middleware.py b/app/core/middleware.py index 7749d26..324ddf0 100644 --- a/app/core/middleware.py +++ b/app/core/middleware.py @@ -83,6 +83,9 @@ class CSRFMiddleware(BaseHTTPMiddleware): status_code=status.HTTP_403_FORBIDDEN, content={"detail": "CSRF token mismatch", "code": "csrf_token_mismatch"}, ) + + # Sliding session: also extend TTL on CSRF-validated unsafe requests + await redis.expire(f"session:{session_id}", settings.session_ttl_seconds) finally: await redis.close() diff --git a/app/deps.py b/app/deps.py index ac394aa..b44de19 100644 --- a/app/deps.py +++ b/app/deps.py @@ -10,7 +10,7 @@ from fastapi import Depends, HTTPException, Request, status from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings -from app.core.auth import get_redis, get_session_data +from app.core.auth import get_redis, get_session_data, refresh_session_ttl from app.core.db import get_db, set_tenant_context @@ -44,6 +44,9 @@ async def get_current_user( detail={"detail": "Session expired or invalid", "code": "session_invalid"}, ) + # Sliding session: extend TTL on each authenticated request + await refresh_session_ttl(redis, session_id) + if not session_data.get("is_active", True): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/app/routes/auth.py b/app/routes/auth.py index 967a316..7c868d0 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -133,7 +133,18 @@ async def me( detail={"detail": "Session expired or invalid", "code": "session_invalid"}, ) - return info + from fastapi.responses import JSONResponse + resp = JSONResponse(status_code=200, content=info) + resp.set_cookie( + key=settings.session_cookie_name, + value=session_id, + httponly=settings.session_cookie_httponly, + secure=settings.session_cookie_secure, + samesite=settings.session_cookie_samesite, + max_age=settings.session_ttl_seconds, + path="/", + ) + return resp @router.get("/me/permissions")