Authentication answers who you are; authorization answers what you may do. FastAPI supports two patterns: OAuth2 scopes (fine-grained permissions in the token, spec-compliant) and role checks (simpler, application-level).
from fastapi import FastAPI, Depends, HTTPException, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"items:read": "Read items",
"items:write": "Create or update items",
"admin": "Admin operations",
},
)
def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
payload = decode_jwt(token) # as in the previous page
token_scopes = payload.get("scopes", [])
# check that the token has every scope required by the endpoint
for scope in security_scopes.scopes:
if scope not in token_scopes:
raise HTTPException(
status_code=403,
detail=f"Not enough permissions. Required: {security_scopes.scope_str}",
headers={"WWW-Authenticate": f"Bearer scope=\"{security_scopes.scope_str}\""},
)
return get_user(payload["sub"])
@app.delete("/items/{item_id}", dependencies=[Security(get_current_user, scopes=["items:write"])])
def delete_item(item_id: int):
return {"deleted": item_id}
Security(get_current_user, scopes=[...]) is Depends plus required scopes.SecurityScopes collects the union of all scopes required up to that point (router + endpoint), so one dependency can enforce everything.scope=items:read items:write — grant only what the user actually has.ROLE_SCOPES = {
"user": ["items:read"],
"editor": ["items:read", "items:write"],
"admin": ["items:read", "items:write", "admin"],
}
@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
requested = set(form_data.scopes) # client-requested scopes
allowed = set(ROLE_SCOPES[user.role])
granted = list(requested & allowed) # intersection
token = create_access_token({"sub": str(user.id), "scopes": granted})
return {"access_token": token, "token_type": "bearer"}
Scopes requested by the client arrive in the token request as a space-separated string: scope=items:read items:write — grant only what the user actually has.
from functools import wraps
def require_role(*roles: str):
async def checker(user = Depends(get_current_user)):
if user.role not in roles:
raise HTTPException(status_code=403, detail="Forbidden for your role")
return user
return checker
admin_router = APIRouter(
prefix="/admin",
dependencies=[Depends(require_role("admin"))],
)
@admin_router.post("/users/")
def admin_create_user(...): ...
require_role("admin", "editor")) parametrizes the check; apply it per-endpoint or per-router.@app.put("/items/{item_id}")
def update_item(item_id: int, item: ItemUpdate, user = Depends(get_current_user)):
stored = items_repo.get(item_id)
if stored is None:
raise HTTPException(status_code=404, detail="Item not found")
if stored.owner_id != user.id and user.role != "admin":
raise HTTPException(status_code=403, detail="Not the owner")
...
Scopes/roles answer "can this user call this endpoint?"; ownership checks answer "can they touch this row?" — both are needed, and the second lives in the endpoint (or a dedicated dependency) because it needs the resource.
A: 401 Unauthorized when there is no valid authentication (missing/invalid/expired token) and 403 Forbidden when the user is authenticated but lacks permission. Mixing them up confuses clients' retry logic.
A: Yes — the scopes={...} dictionary on OAuth2PasswordBearer renders as checkboxes in the "Authorize" dialog, so testers can obtain tokens with specific scopes and the docs show which endpoints need which scopes.
A: Route-level "can call this" checks belong in dependencies (reusable, visible in one place). Resource-level checks ("is this their row?") usually stay in the endpoint or a dependency that receives the path parameter — a dependency can read path/query parameters too, e.g. def owns_item(item_id: int, user=Depends(get_current_user)).
FastAPI makes authorization a first-class citizen: declare scopes on OAuth2PasswordBearer, enforce them with Security(...) and SecurityScopes, grant at token time from roles, and add ownership checks for object-level rules. The auth story from the last two pages is now complete.