# Combining sources

## `MultiAuth`

`MultiAuth` puts several sources on one route: browsers send a cookie, scripts send a token, both reach the same endpoints.

```python
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:

```python
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`.

> [!WARNING]
>
> `optional()` cannot be combined with [scopes](<https://fastapi-multiauth.d3vyce.fr/guide/scopes/index.md>) or [step-up](<https://fastapi-multiauth.d3vyce.fr/guide/step-up/index.md>) guards: an anonymous visitor can never satisfy them.
