API keys and Basic¶
API keys¶
APIKeyHeaderAuth reads a named header:
from fastapi import FastAPI, Security
from fastapi_multiauth import APIKeyHeaderAuth, UnauthorizedError, hash_token
CLIENTS = {hash_token("key-for-reporting"): "reporting-service"}
async def validate_api_key(key: str) -> str:
client = CLIENTS.get(hash_token(key))
if client is None:
raise UnauthorizedError()
return client
api_key = APIKeyHeaderAuth(name="X-API-Key", validator=validate_api_key)
app = FastAPI()
@app.get("/reports")
async def reports(client: str = Security(api_key)):
return {"client": client}
Generate and store keys like bearer tokens: random, and hashed at rest.
APIKeyQueryAuth is the same for a query parameter (?api_key=...). Prefer the header: query strings end up in logs and browser history.
Basic auth¶
HTTPBasicAuth calls your validator with username, password:
import os
import secrets
from fastapi import FastAPI, Security
from fastapi_multiauth import HTTPBasicAuth, UnauthorizedError
METRICS_PASSWORD = os.environ["METRICS_PASSWORD"]
async def validate_basic(username: str, password: str) -> str:
username_ok = secrets.compare_digest(username.encode(), b"prometheus")
password_ok = secrets.compare_digest(password.encode(), METRICS_PASSWORD.encode())
if not (username_ok and password_ok):
raise UnauthorizedError()
return username
basic = HTTPBasicAuth(validator=validate_basic, realm="metrics")
app = FastAPI()
@app.get("/metrics")
async def metrics(username: str = Security(basic)):
return {"scraped_by": username}
realm is sent in the WWW-Authenticate header, which makes browsers show a login prompt. Compare secrets with secrets.compare_digest, never ==, and compare bytes: it raises TypeError on non-ASCII strings. Check the username and the password both, so the response time does not reveal which one was wrong.