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

python
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:

python
@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.

python
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.