fix: exempt static assets from auth middleware — login page can load CSS/JS/images

This commit is contained in:
Brian Krabach
2026-03-30 08:03:49 -07:00
parent e6add00f0d
commit df3e252f73
2 changed files with 81 additions and 3 deletions
+22 -3
View File
@@ -136,6 +136,20 @@ def authenticate_pam(username: str, password: str) -> bool:
# Paths that bypass auth (login page itself, static assets it needs) # Paths that bypass auth (login page itself, static assets it needs)
_AUTH_EXEMPT_PATHS = {"/login", "/auth/mode", "/auth/logout"} _AUTH_EXEMPT_PATHS = {"/login", "/auth/mode", "/auth/logout"}
# File extensions that are always served without auth — the login page needs
# its own CSS, JS, images, and fonts before the user has a session cookie.
_STATIC_EXTENSIONS = {
".css",
".js",
".svg",
".png",
".ico",
".woff",
".woff2",
".ttf",
".map",
}
# Socket-level localhost addresses — cannot be forged via HTTP headers # Socket-level localhost addresses — cannot be forged via HTTP headers
_LOCALHOST_ADDRS = {"127.0.0.1", "::1"} _LOCALHOST_ADDRS = {"127.0.0.1", "::1"}
@@ -168,12 +182,17 @@ class AuthMiddleware(BaseHTTPMiddleware):
if request.url.path in _AUTH_EXEMPT_PATHS: if request.url.path in _AUTH_EXEMPT_PATHS:
return await call_next(request) return await call_next(request)
# 3. Valid session cookie # 3. Static assets — login page needs its CSS/JS/images before auth
path = request.url.path
if any(path.endswith(ext) for ext in _STATIC_EXTENSIONS):
return await call_next(request)
# 4. Valid session cookie
cookie = request.cookies.get("muxplex_session") cookie = request.cookies.get("muxplex_session")
if cookie and verify_session_cookie(self.secret, cookie, self.ttl_seconds): if cookie and verify_session_cookie(self.secret, cookie, self.ttl_seconds):
return await call_next(request) return await call_next(request)
# 4. Authorization: Basic header # 5. Authorization: Basic header
auth_header = request.headers.get("authorization", "") auth_header = request.headers.get("authorization", "")
if auth_header.lower().startswith("basic "): if auth_header.lower().startswith("basic "):
try: try:
@@ -186,7 +205,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
pass pass
return JSONResponse({"detail": "Invalid credentials"}, status_code=401) return JSONResponse({"detail": "Invalid credentials"}, status_code=401)
# 5. No auth — redirect browsers, 401 for API clients # 6. No auth — redirect browsers, 401 for API clients
accept = request.headers.get("accept", "") accept = request.headers.get("accept", "")
if "application/json" in accept: if "application/json" in accept:
return JSONResponse({"detail": "Authentication required"}, status_code=401) return JSONResponse({"detail": "Authentication required"}, status_code=401)
+59
View File
@@ -1066,3 +1066,62 @@ def test_delete_session_not_found(client, monkeypatch):
response = client.delete("/api/sessions/nonexistent") response = client.delete("/api/sessions/nonexistent")
assert response.status_code == 404 assert response.status_code == 404
# ---------------------------------------------------------------------------
# Issue 1: Static assets exempt from auth middleware
# ---------------------------------------------------------------------------
def test_static_asset_accessible_from_non_localhost_without_auth(monkeypatch):
"""Static assets (.svg, .css, .js etc.) are served without auth from non-localhost.
The login page needs its own CSS/JS/images to render before the user has
authenticated. The auth middleware must exempt static file extensions.
"""
monkeypatch.setenv("MUXPLEX_PASSWORD", "test-pw")
with TestClient(app, base_url="http://192.168.1.1", follow_redirects=False) as c:
response = c.get("/wordmark-on-dark.svg")
assert response.status_code == 200, (
f"Expected 200 for static asset from non-localhost, got {response.status_code}"
)
def test_css_asset_accessible_from_non_localhost_without_auth(monkeypatch):
"""CSS files are served without auth from non-localhost."""
monkeypatch.setenv("MUXPLEX_PASSWORD", "test-pw")
with TestClient(app, base_url="http://192.168.1.1", follow_redirects=False) as c:
response = c.get("/style.css")
assert response.status_code == 200, (
f"Expected 200 for CSS from non-localhost, got {response.status_code}"
)
# ---------------------------------------------------------------------------
# Issue 2: Hostname in page title
# ---------------------------------------------------------------------------
def test_index_page_title_contains_hostname(client):
"""GET / returns HTML with hostname in page title (e.g. 'myhost — muxplex')."""
import socket
hostname = socket.gethostname().split(".")[0]
response = client.get("/")
assert response.status_code == 200
assert hostname in response.text, (
f"Expected hostname '{hostname}' in title of index page"
)
assert "muxplex" in response.text
def test_login_page_title_contains_hostname(client):
"""GET /login returns HTML with hostname in page title (e.g. 'Sign in — myhost — muxplex')."""
import socket
hostname = socket.gethostname().split(".")[0]
response = client.get("/login")
assert response.status_code == 200
assert hostname in response.text, (
f"Expected hostname '{hostname}' in title of login page"
)