diff --git a/app/core/middleware/security_headers.py b/app/core/middleware/security_headers.py index 23829ce..681cfd4 100644 --- a/app/core/middleware/security_headers.py +++ b/app/core/middleware/security_headers.py @@ -1,8 +1,15 @@ from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request +from app.core.security.session_auth import SESSION_USER_ID_KEY + + class SecurityHeadersMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): + # Capture authentication state before the route runs. Logout clears the + # session during the request, but its response must still be non-cacheable. + was_authenticated = bool(request.session.get(SESSION_USER_ID_KEY)) + resp = await call_next(request) resp.headers["X-Content-Type-Options"] = "nosniff" resp.headers["X-Frame-Options"] = "DENY" @@ -18,4 +25,16 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): "connect-src 'self'; " "frame-ancestors 'none';" ) + + # Authenticated ERP pages contain client, PAN, document, billing and + # engagement data. Prevent browsers and intermediary caches from storing + # them, including responses generated immediately before logout. + is_authenticated = bool(request.session.get(SESSION_USER_ID_KEY)) + if was_authenticated or is_authenticated: + resp.headers["Cache-Control"] = ( + "no-store, no-cache, must-revalidate, private, max-age=0" + ) + resp.headers["Pragma"] = "no-cache" + resp.headers["Expires"] = "0" + return resp diff --git a/app/ui/routes/auth.py b/app/ui/routes/auth.py index 3aee151..3b615f7 100644 --- a/app/ui/routes/auth.py +++ b/app/ui/routes/auth.py @@ -910,5 +910,21 @@ def change_password_required(request: Request): @router.get("/logout") def logout(request: Request): request.session.clear() - return RedirectResponse(url="/login", status_code=303) + + response = RedirectResponse(url="/login", status_code=303) + settings = get_settings() + response.delete_cookie( + key=settings.COOKIE_SESSION_NAME, + path="/", + secure=settings.COOKIE_SECURE, + httponly=True, + samesite=settings.COOKIE_SAMESITE, + ) + response.headers["Cache-Control"] = ( + "no-store, no-cache, must-revalidate, private, max-age=0" + ) + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + response.headers["Clear-Site-Data"] = '"cache"' + return response diff --git a/app/ui/templates/base/layout.html b/app/ui/templates/base/layout.html index 85dc337..f369c3c 100644 --- a/app/ui/templates/base/layout.html +++ b/app/ui/templates/base/layout.html @@ -748,5 +748,26 @@ })(); +{% if full_auth %} + +{% endif %} +