# Scopes

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

```python
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`](<https://fastapi-multiauth.d3vyce.fr/guide/jwt-and-oauth2/index.md>) 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](<https://fastapi-multiauth.d3vyce.fr/guide/jwt-and-oauth2/#a-real-authorize-button>) can also publish the list of available scopes, which gives `/docs` its checkboxes.
