Logging Monitoring Failures in Fastapi with Basic Auth
Logging Monitoring Failures in Fastapi with Basic Auth — how this specific combination creates or exposes the vulnerability
When Basic Authentication is used in FastAPI without structured logging and active monitoring, security events that should be surfaced can be missed or rendered useless. Basic Auth sends credentials in an Authorization header on each request; if the server does not log enough context and does not monitor those logs, attackers can operate undetected.
Key gaps appear in three dimensions:
- Detection: Without logging of authentication successes and failures, you cannot identify brute-force attempts, credential spraying, or reuse of stolen credentials. A missing log line for a 401 means an attacker can try passwords without visibility.
- Alerting: If logs are not monitored with rules that raise alerts on anomalies (e.g., many 401s from one IP, a single user failing from multiple locations), suspicious behavior escalates to a breach.
- Forensics: In an incident, poor logs (missing usernames, client IPs, timestamps, paths, and status codes) prevent reconstructing the chain of events. This hinders root cause analysis and compliance reporting.
These issues are especially relevant because FastAPI does not enforce logging by default; you must add it. An endpoint that uses Basic Auth but lacks audit logs provides a weak defense-in-depth posture, even if authentication itself is technically correct.
Example of insufficient logging in FastAPI with Basic Auth:
from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import HTTPBasic, HTTPBasicCredentials import logging logging.basicConfig(level="INFO") logger = logging.getLogger(__name__) app = FastAPI() security = HTTPBasic() USERS = {"alice": "secret123"} def get_current_user(credentials: HTTPBasicCredentials = Depends(security)): correct = USERS.get(credentials.username) == credentials.password if not correct: # Missing: structured log with username, client host, path, and outcome logger.warning("Auth failed") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Basic"}, ) # Missing: success log with username and client context return credentials.username @app.get("/secure") def read_secure(user: str = Depends(get_current_user)): return {"message": f"Hello {user}"}In this example, the logs lack structured fields (username, IP, endpoint, timestamp), making it hard to monitor patterns. Without integrating these logs into a monitoring system that triggers alerts on repeated 401s or impossible travel, the monitoring control is ineffective. middleBrick scans can surface such weaknesses by checking authentication coverage and log-related findings in the unauthenticated attack surface.
Basic Auth-Specific Remediation in Fastapi — concrete code fixes
Remediation focuses on three actions: structured logging of authentication events, rate-limiting to reduce brute-force impact, and ensuring logs contain enough detail for monitoring and forensics. Below are concrete, working examples.
1) Structured logging for auth successes and failures
Log usernames (or a hashed representation), client IP, path, HTTP method, status, and a unique request ID to correlate logs and support alerting.
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.middleware.base import BaseHTTPMiddleware
import logging
import time
import uuid
logging.basicConfig(level="INFO", format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
app = FastAPI()
security = HTTPBasic()
USERS = {"alice": "secret123"}
class CorrelationMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
request.state.correlation_id = str(uuid.uuid4())
response = await call_next(request)
return response
app.add_middleware(CorrelationMiddleware)
def get_current_user(credentials: HTTPBasicCredentials = Depends(security), request: Request = None):
username = credentials.username
client_host = request.client.host if request and request.client else "unknown"
req_id = getattr(request.state, "correlation_id", "unknown") if request else "unknown"
correct = USERS.get(username) == credentials.password
if not correct:
logger.warning(
"auth_failure",
extra={
"event": "auth_failure",
"username": username,
"client_ip": client_host,
"method": request.method if request else "unknown",
"path": request.url.path if request else "unknown",
"correlation_id": req_id,
},
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
logger.info(
"auth_success",
extra={
"event": "auth_success",
"username": username,
"client_ip": client_host,
"method": request.method if request else "unknown",
"path": request.url.path if request else "unknown",
"correlation_id": req_id,
},
)
return username
@app.get("/secure")
def read_secure(user: str = Depends(lambda r: get_current_user(request=r))):
return {"message": f"Hello {user}"}
2) Add rate-limiting to mitigate brute-force
Use a simple in-memory store for demonstration; in production, use Redis or a distributed store.
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from collections import defaultdict
import time
app = FastAPI()
security = HTTPBasic()
USERS = {"alice": "secret123"}
failed_attempts = defaultdict(list)
RATE_LIMIT_WINDOW = 60 # seconds
MAX_ATTEMPTS = 5
def is_rate_limited(client_ip: str) -> bool:
now = time.time()
attempts = failed_attempts[client_ip]
# clean old attempts
failed_attempts[client_ip] = [t for t in attempts if now - t < RATE_LIMIT_WINDOW]
return len(failed_attempts[client_ip]) >= MAX_ATTEMPTS
def get_current_user(credentials: HTTPBasicCredentials = Depends(security), request: Request = None):
client_ip = request.client.host if request and request.client else "unknown"
if is_rate_limited(client_ip):
raise HTTPException(status_code=429, detail="Too many requests")
correct = USERS.get(credentials.username) == credentials.password
if not correct:
failed_attempts[client_ip].append(time.time())
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
# success: optionally clear failed attempts
failed_attempts[client_ip].clear()
return credentials.username
3) Monitoring and alerting guidance
Ensure your logging pipeline indexes fields like event, username, and client_ip so you can create alerts such as: trigger if >10 auth_failure events from the same client_ip within 5 minutes. Regularly review logs for patterns like credential reuse across endpoints, which basic auth does not protect against replay without additional transport protections.
middleBrick can be used in the Pro plan for continuous monitoring of such authentication surfaces, and the GitHub Action can fail builds if new endpoints introduce Basic Auth without corresponding logging controls.