diff --git a/backend/app/middleware/auth.py b/backend/app/middleware/auth.py index d5fc338..3eec32b 100644 --- a/backend/app/middleware/auth.py +++ b/backend/app/middleware/auth.py @@ -37,6 +37,24 @@ _PUBLIC_PREFIXES: tuple[str, ...] = ( "/openapi", ) +# File extensions for static assets that are always publicly accessible +_STATIC_EXTENSIONS: tuple[str, ...] = ( + ".ico", + ".png", + ".jpg", + ".jpeg", + ".gif", + ".svg", + ".webp", + ".css", + ".js", + ".woff", + ".woff2", + ".ttf", + ".eot", + ".map", +) + class AuthRedirectMiddleware(BaseHTTPMiddleware): """ @@ -68,6 +86,8 @@ class AuthRedirectMiddleware(BaseHTTPMiddleware): return await call_next(request) if any(path.startswith(p) for p in _PUBLIC_PREFIXES): return await call_next(request) + if any(path.endswith(ext) for ext in _STATIC_EXTENSIONS): + return await call_next(request) # ── 2. Valid session cookie ─────────────────────────────────────────── token = request.cookies.get(SESSION_COOKIE) diff --git a/backend/app/tests/test_auth.py b/backend/app/tests/test_auth.py index 6e123fc..782c2d4 100644 --- a/backend/app/tests/test_auth.py +++ b/backend/app/tests/test_auth.py @@ -293,3 +293,39 @@ class TestAuthDisabled: # never be a 302 redirect from the middleware. res = client.get("/settings", follow_redirects=False) assert res.status_code != 302 + + +# ── Static asset bypass ─────────────────────────────────────────────────────── + + +class TestStaticAssetBypass: + """Static assets must never be redirected to the login page.""" + + @staticmethod + def _logto_configured_mock(): + mock_cfg = MagicMock() + mock_cfg.AUTH_DISABLED = False + mock_cfg.logto_configured = True + return mock_cfg + + def test_favicon_not_redirected_to_login(self, client: TestClient): + """GET /favicon.ico without a session must pass through (not redirect to /login).""" + with patch("app.core.config.get_settings") as mock_get_settings: + mock_get_settings.return_value = self._logto_configured_mock() + res = client.get("/favicon.ico", follow_redirects=False) + assert res.status_code != 302 + + def test_png_asset_not_redirected_to_login(self, client: TestClient): + """GET /logo.png without a session must pass through.""" + with patch("app.core.config.get_settings") as mock_get_settings: + mock_get_settings.return_value = self._logto_configured_mock() + res = client.get("/logo.png", follow_redirects=False) + assert res.status_code != 302 + + def test_protected_page_still_redirected(self, client: TestClient): + """GET /dashboard without a session must still redirect to /login.""" + with patch("app.core.config.get_settings") as mock_get_settings: + mock_get_settings.return_value = self._logto_configured_mock() + res = client.get("/dashboard", follow_redirects=False) + assert res.status_code == 302 + assert res.headers["location"].startswith("/login")