Custom sources¶
When no built-in source reads your credential, subclass AuthSource and write two methods:
from fastapi import FastAPI, Security
from fastapi.requests import HTTPConnection
from fastapi.security import APIKeyHeader
from fastapi_multiauth import AuthSource, UnauthorizedError
CLIENTS = {"ab:cd:ef": "billing-service"}
TRUSTED_PROXIES = {"10.0.0.1"}
class ClientCertAuth(AuthSource):
def __init__(self) -> None:
super().__init__(
scheme=APIKeyHeader(name="X-Client-Cert", scheme_name="ClientCert")
)
async def extract(self, request: HTTPConnection) -> str | None:
if request.client is None or request.client.host not in TRUSTED_PROXIES:
return None
return request.headers.get("X-Client-Cert") or None
async def authenticate(self, credential: str) -> str:
client = CLIENTS.get(credential)
if client is None:
raise UnauthorizedError()
return client
client_cert = ClientCertAuth()
app = FastAPI()
@app.get("/invoices")
async def invoices(client: str = Security(client_cert)):
return {"client": client}
extract(request)- Find the credential in the request. Return
Nonewhen it is absent, soMultiAuthandoptional()work. authenticate(credential)- Return the user or raise, like a validator.
X-Client-Cert comes from the proxy that terminates TLS, so extract() ignores it on connections from anywhere else. The proxy must overwrite any value the client sent. If uvicorn trusts the proxy through --forwarded-allow-ips, request.client is the original client's address instead, and this check refuses every request.
The scheme passed to super().__init__() is only used for OpenAPI. Your source works with MultiAuth, optional(), WebSockets, logging and auditing like the built-in ones.
To check scopes, override authenticate_scoped(). To use fresh() or stepped_up() without a reader, override authenticated_at() or authenticated_acr(). See the API reference.