Once requests are [authenticated](/guides/python/authentication), the next step
is **authorization**: deciding whether the authenticated user is allowed to
perform the action. Authdog resolves *who* the user is; you decide *what* they
can do based on the roles and permissions on the user object.

This guide builds a small, reusable access-control layer for FastAPI on top of
`authdog.require_auth`.

## Where roles and permissions come from

`require_auth` returns the `user` object from the identity provider's `userinfo`
response. Role and permission claims travel on that object — for example:

```json
{
  "id": "usr_123",
  "email": "ada@example.com",
  "roles": ["admin"],
  "permissions": ["posts:read", "posts:write"]
}
```

> The exact claim names depend on how your Authdog environment is configured.
> Adjust the accessors below to match your token's shape.

## A `require_role` dependency

The pattern is a **dependency factory**: a function that takes the required role
and returns a FastAPI dependency. It depends on `require_auth` first — so
authentication is always enforced — then checks the claim:

```python
from typing import Any
from fastapi import Depends, HTTPException, status
from authdog.fastapi import Authdog

authdog = Authdog(public_key=os.environ["PK_AUTHDOG"])

def require_role(*roles: str):
    """FastAPI dependency: 401 if unauthenticated, 403 if the role is missing."""
    async def _dependency(user: Any = Depends(authdog.require_auth)) -> Any:
        user_roles = set(user.get("roles", []))
        if not user_roles.intersection(roles):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Insufficient role",
            )
        return user
    return _dependency
```

Use it on any route:

```python
@app.get("/admin", dependencies=[Depends(require_role("admin"))])
async def admin_dashboard():
    return {"ok": True}

# Or capture the user when you need it:
@app.delete("/posts/{post_id}")
async def delete_post(post_id: str, user=Depends(require_role("admin", "editor"))):
    ...
```

## Permission-based checks

Fine-grained authorization checks a **permission** rather than a role. Same
factory shape:

```python
def require_permission(*required: str):
    async def _dependency(user: Any = Depends(authdog.require_auth)) -> Any:
        granted = set(user.get("permissions", []))
        missing = set(required) - granted
        if missing:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Missing permission(s): {', '.join(sorted(missing))}",
            )
        return user
    return _dependency

@app.post("/posts", dependencies=[Depends(require_permission("posts:write"))])
async def create_post():
    ...
```

## Resource-level (ownership) checks

Role and permission gates run before the handler. Ownership checks need the
resource, so do them **inside** the handler once you have the authenticated
user:

```python
@app.patch("/posts/{post_id}")
async def update_post(post_id: str, user=Depends(authdog.require_auth)):
    post = await db.get_post(post_id)
    if post is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND)
    is_owner = post.author_id == user.get("id")
    is_admin = "admin" in user.get("roles", [])
    if not (is_owner or is_admin):
        raise HTTPException(status.HTTP_403_FORBIDDEN, "Not your post")
    return await db.update_post(post_id, ...)
```

## Choosing status codes

- **401 Unauthorized** — no valid session. Raised for you by `require_auth`.
- **403 Forbidden** — authenticated, but not allowed. Raised by your role /
  permission checks.

Keeping these distinct lets your frontend redirect to sign-in on a 401 and show
an "access denied" message on a 403.

## Next steps

- [Basic authentication](/guides/python/authentication) — revisit the session,
  gate, and logout primitives.
- [Python SDK overview](/product/python) — the full framework matrix.
