Combining sources¶
MultiAuth¶
MultiAuth puts several sources on one route: browsers send a cookie, scripts send a token, both reach the same endpoints.
import os
from fastapi import FastAPI, Security
from fastapi_multiauth import (
APIKeyCookieAuth,
HTTPBearerAuth,
MultiAuth,
UnauthorizedError,
hash_token,
)
SECRET_KEY = os.environ["SECRET_KEY"]
TOKENS = {hash_token("user_alice-token"): "alice"}
async def validate_token(token: str) -> str:
username = TOKENS.get(hash_token(token))
if username is None:
raise UnauthorizedError()
return username
async def validate_session(username: str) -> str:
return username
bearer = HTTPBearerAuth(validator=validate_token, prefix="user_")
session = APIKeyCookieAuth(
name="__Host-session", validator=validate_session, secret_key=SECRET_KEY
)
auth = MultiAuth(bearer, session)
app = FastAPI()
@app.get("/me")
async def me(username: str = Security(auth)):
return {"username": username}
Sources are tried in order, and the first one that finds a credential decides:
- If that credential is invalid, the request is refused. It does not fall through to the next source.
- With no credential at all, the 401 lists every source's challenge.
- All sources appear in
/docs.
Put the bearer source first: when a request carries both a token and a cookie, the token the client sent on purpose wins over the cookie the browser attached on its own.
Optional authentication¶
optional() returns a copy that gives None instead of a 401 when the request has no credential. Use it for pages that work for everyone but show more to signed-in users:
from typing import Annotated
from fastapi import FastAPI, Security
from fastapi_multiauth import HTTPBearerAuth, UnauthorizedError, hash_token
EDITORS = {hash_token("editor-token"): "alice"}
async def validate_token(token: str) -> str:
editor = EDITORS.get(hash_token(token))
if editor is None:
raise UnauthorizedError()
return editor
bearer = HTTPBearerAuth(validator=validate_token)
MaybeEditor = Annotated[str | None, Security(bearer.optional())]
app = FastAPI()
@app.get("/articles/{slug}")
async def article(slug: str, editor: MaybeEditor):
return {"slug": slug, "show_drafts": editor is not None}
A credential that is present but invalid still gets a 401: optional() never turns a bad token into an anonymous visitor. It works on single sources and on MultiAuth.