43 lines
1.5 KiB
Python
43 lines
1.5 KiB
Python
import hmac
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
|
|
from ..agent_auth import credential_from_token
|
|
from ..errors import error_response
|
|
from ..settings import get_settings
|
|
|
|
NO_AUTH_PATHS = {"/api/v1/health", "/api/v1/ready", "/metrics"}
|
|
|
|
AGENT_PATHS = {"/api/v1/heartbeat", "/api/v1/events"}
|
|
|
|
|
|
def _match_token(presented: str, accepted: list[str]) -> bool:
|
|
matched = False
|
|
# Constant-time across all accepted tokens to avoid timing oracle.
|
|
for t in accepted:
|
|
if hmac.compare_digest(presented, t):
|
|
matched = True
|
|
return matched
|
|
|
|
|
|
class BearerAuthMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
path = request.url.path
|
|
if path in NO_AUTH_PATHS:
|
|
return await call_next(request)
|
|
|
|
header = request.headers.get("authorization")
|
|
if not header or not header.lower().startswith("bearer "):
|
|
return error_response(request, 401, "unauthorized", "authentication required")
|
|
token = header.split(" ", 1)[1].strip()
|
|
|
|
if path in AGENT_PATHS:
|
|
if len(token) < 32 or len(token) > 512:
|
|
return error_response(request, 401, "unauthorized", "authentication required")
|
|
request.state.agent_credential = credential_from_token(token)
|
|
else:
|
|
if not _match_token(token, get_settings().auth_tokens):
|
|
return error_response(request, 401, "unauthorized", "authentication required")
|
|
return await call_next(request)
|