Skip to content

Scopes

Declare the scopes a route needs with Security(source, scopes=[...]). A validator with a scopes parameter receives them:

from fastapi import FastAPI, Security

from fastapi_multiauth import (
    ForbiddenError,
    HTTPBearerAuth,
    UnauthorizedError,
    hash_token,
)

TOKENS = {
    hash_token("writer-token"): ("alice", {"posts:read", "posts:write"}),
    hash_token("reader-token"): ("bob", {"posts:read"}),
}


async def validate_token(token: str, scopes: list[str]) -> str:
    row = TOKENS.get(hash_token(token))
    if row is None:
        raise UnauthorizedError()
    username, granted = row
    if not set(scopes) <= granted:
        raise ForbiddenError("Missing scope")
    return username


bearer = HTTPBearerAuth(validator=validate_token)

app = FastAPI()


@app.get("/posts")
async def list_posts(username: str = Security(bearer, scopes=["posts:read"])):
    return []


@app.post("/posts")
async def create_post(username: str = Security(bearer, scopes=["posts:write"])):
    return {"author": username}

Raise ForbiddenError (403) for a missing scope: the token is valid, it just cannot do this. JWTValidator does this check for you.

Fails closed

If a route declares scopes but the validator has no scopes parameter, the request fails with an error instead of skipping the check. A forgotten parameter shows up in your first test, not as an open route in production.

The same goes for MultiAuth: every source must be able to check scopes, otherwise the route would be protected or not depending on which credential the client sent.

In /docs

Scopes appear on every route's security requirement. Only the OAuth 2.0 sources can also publish the list of available scopes, which gives /docs its checkboxes.