feat: add Bearer token auth for federation in AuthMiddleware
This commit is contained in:
@@ -3,6 +3,8 @@ muxplex authentication — password and signing secret file management.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hmac
|
||||
import logging
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
@@ -11,6 +13,8 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, RedirectResponse, Response
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config directory
|
||||
@@ -164,12 +168,14 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
secret: str,
|
||||
ttl_seconds: int,
|
||||
password: str = "",
|
||||
federation_key: str = "",
|
||||
):
|
||||
super().__init__(app)
|
||||
self.auth_mode = auth_mode
|
||||
self.secret = secret
|
||||
self.ttl_seconds = ttl_seconds
|
||||
self.password = password
|
||||
self.federation_key = federation_key
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
# 1. Localhost bypass — client.host is the socket-level IP and cannot
|
||||
@@ -192,6 +198,14 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
if cookie and verify_session_cookie(self.secret, cookie, self.ttl_seconds):
|
||||
return await call_next(request)
|
||||
|
||||
# 4a. Bearer token (server-to-server federation)
|
||||
auth_header = request.headers.get("authorization", "")
|
||||
if self.federation_key and auth_header.lower().startswith("bearer "):
|
||||
token = auth_header[7:]
|
||||
if hmac.compare_digest(token, self.federation_key):
|
||||
return await call_next(request)
|
||||
_log.warning("federation: rejected Bearer from %s", client_host)
|
||||
|
||||
# 4b. X-Muxplex-Token header (for cross-origin federation)
|
||||
token_header = request.headers.get("x-muxplex-token")
|
||||
if token_header:
|
||||
|
||||
@@ -484,3 +484,68 @@ def test_middleware_invalid_token_header_falls_through_to_redirect():
|
||||
response = client.get("/protected", headers={"X-Muxplex-Token": "bad.token.value"})
|
||||
# Invalid token: should NOT pass auth, falls through to redirect or 401
|
||||
assert response.status_code in (307, 401)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bearer token auth (server-to-server federation)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_federation_app(federation_key: str = "fed-key") -> FastAPI:
|
||||
"""Create a minimal FastAPI app with AuthMiddleware configured with a federation_key."""
|
||||
test_app = FastAPI()
|
||||
|
||||
test_app.add_middleware(
|
||||
AuthMiddleware,
|
||||
auth_mode="password",
|
||||
secret="test-secret",
|
||||
ttl_seconds=3600,
|
||||
password="test-pw",
|
||||
federation_key=federation_key,
|
||||
)
|
||||
|
||||
@test_app.get("/protected")
|
||||
async def protected():
|
||||
return PlainTextResponse("OK")
|
||||
|
||||
return test_app
|
||||
|
||||
|
||||
def test_middleware_valid_bearer_token_passes():
|
||||
"""Non-localhost with valid Bearer token and federation_key configured passes through (200)."""
|
||||
app = _make_federation_app(federation_key="my-federation-secret")
|
||||
client = TestClient(app, base_url="http://192.168.1.1")
|
||||
response = client.get(
|
||||
"/protected", headers={"Authorization": "Bearer my-federation-secret"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.text == "OK"
|
||||
|
||||
|
||||
def test_middleware_invalid_bearer_token_falls_through():
|
||||
"""Non-localhost with wrong Bearer token falls through to 401 (not accepted)."""
|
||||
app = _make_federation_app(federation_key="my-federation-secret")
|
||||
client = TestClient(app, base_url="http://192.168.1.1", follow_redirects=False)
|
||||
response = client.get(
|
||||
"/protected",
|
||||
headers={
|
||||
"Authorization": "Bearer wrong-token",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_middleware_bearer_skipped_when_no_federation_key():
|
||||
"""Empty federation_key means Bearer tokens are never accepted (falls through to 401)."""
|
||||
app = _make_federation_app(federation_key="")
|
||||
client = TestClient(app, base_url="http://192.168.1.1", follow_redirects=False)
|
||||
response = client.get(
|
||||
"/protected",
|
||||
headers={
|
||||
"Authorization": "Bearer any-token-value",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
# Bearer should be skipped when federation_key is empty, falls through to 401
|
||||
assert response.status_code == 401
|
||||
|
||||
Reference in New Issue
Block a user