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)
|
# 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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user