fix: exempt static assets from auth middleware — login page can load CSS/JS/images
This commit is contained in:
+22
-3
@@ -136,6 +136,20 @@ def authenticate_pam(username: str, password: str) -> bool:
|
||||
# Paths that bypass auth (login page itself, static assets it needs)
|
||||
_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
|
||||
_LOCALHOST_ADDRS = {"127.0.0.1", "::1"}
|
||||
|
||||
@@ -168,12 +182,17 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
if request.url.path in _AUTH_EXEMPT_PATHS:
|
||||
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")
|
||||
if cookie and verify_session_cookie(self.secret, cookie, self.ttl_seconds):
|
||||
return await call_next(request)
|
||||
|
||||
# 4. Authorization: Basic header
|
||||
# 5. Authorization: Basic header
|
||||
auth_header = request.headers.get("authorization", "")
|
||||
if auth_header.lower().startswith("basic "):
|
||||
try:
|
||||
@@ -186,7 +205,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
pass
|
||||
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", "")
|
||||
if "application/json" in accept:
|
||||
return JSONResponse({"detail": "Authentication required"}, status_code=401)
|
||||
|
||||
@@ -1066,3 +1066,62 @@ def test_delete_session_not_found(client, monkeypatch):
|
||||
|
||||
response = client.delete("/api/sessions/nonexistent")
|
||||
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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user