Permissions and Authorization
Introduction
Authentication answers who is calling—authorization answers what they may do. After JWT identifies a user, you enforce roles (admin vs user) and resource ownership (only edit your own todos). FastAPI implements authorization with dependency functions layered on get_current_user.
Prerequisites
Roles on User Model
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String
from app.db.session import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(80), unique=True)
hashed_password: Mapped[str] = mapped_column(String(255))
role: Mapped[str] = mapped_column(String(20), default="user")Valid roles: user, admin—or enum in application code.
Seed admin in migration or CLI—never hardcode admin password in source.
Role Dependencies
app/api/deps.py:
Usage:
@router.get("/admin/stats")
def admin_stats(admin: User = Depends(require_admin)):
return {"secret": "metrics"}
@router.get("/moderation/queue")
def mod_queue(user: User = Depends(require_roles("admin", "moderator"))):
return {"queue": []}Code explanation:
- 401 — not authenticated (handled in
get_current_user) - 403 — authenticated but not allowed
Resource Ownership
Todo belongs to one user:
Assumes get_current_user is defined in the same deps.py module above these helpers.
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.session import Base
class Todo(Base):
__tablename__ = "todos"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"))Fetch with ownership check:
Admins bypass owner check—explicit product decision; document it.
Optional Authentication
Public route with enhanced behavior when logged in:
Scopes (OAuth2 Advanced)
OAuth2 scopes in token claims for fine-grained API access—FastAPI supports Security(scopes=[...]) with OAuth2PasswordBearer(scopes=...). Use when one token grants partial permissions; JWT chapter foundation required.
Compare With Flask-Login
Flask uses @login_required and session cookies—see Flask-Login. FastAPI API projects use JWT + Depends instead.
FAQ
403 vs 404 on other user's resource?
404 hides existence; 403 confirms resource exists—pick policy for your API.
Store permissions in JWT?
Possible via claims—requires token reissue when permissions change; DB lookup per request is simpler for small apps.
Multiple tenants?
Add tenant_id to models and filter every query—dependency get_current_tenant.
Unit test forbidden?
Override get_current_user to return non-admin user; expect 403.
RBAC library?
Casbin, OSO—for complex policies; dependencies stay readable for CRUD apps.
Delete user with todos?
ON DELETE CASCADE or soft-delete—design in Alembic migration.