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 base64
|
||||||
|
import hmac
|
||||||
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -11,6 +13,8 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import JSONResponse, RedirectResponse, Response
|
from starlette.responses import JSONResponse, RedirectResponse, Response
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Config directory
|
# Config directory
|
||||||
@@ -164,12 +168,14 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
secret: str,
|
secret: str,
|
||||||
ttl_seconds: int,
|
ttl_seconds: int,
|
||||||
password: str = "",
|
password: str = "",
|
||||||
|
federation_key: str = "",
|
||||||
):
|
):
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.auth_mode = auth_mode
|
self.auth_mode = auth_mode
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
self.ttl_seconds = ttl_seconds
|
self.ttl_seconds = ttl_seconds
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.federation_key = federation_key
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next) -> Response:
|
async def dispatch(self, request: Request, call_next) -> Response:
|
||||||
# 1. Localhost bypass — client.host is the socket-level IP and cannot
|
# 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):
|
if cookie and verify_session_cookie(self.secret, cookie, self.ttl_seconds):
|
||||||
return await call_next(request)
|
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)
|
# 4b. X-Muxplex-Token header (for cross-origin federation)
|
||||||
token_header = request.headers.get("x-muxplex-token")
|
token_header = request.headers.get("x-muxplex-token")
|
||||||
if token_header:
|
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"})
|
response = client.get("/protected", headers={"X-Muxplex-Token": "bad.token.value"})
|
||||||
# Invalid token: should NOT pass auth, falls through to redirect or 401
|
# Invalid token: should NOT pass auth, falls through to redirect or 401
|
||||||
assert response.status_code in (307, 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