Skip to content

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.