FastAPI Role-Based Access Control

Login proves who a person is. Access control decides what that person may do. Role-based access control, often shortened to RBAC, groups permissions into roles such as admin, editor, and viewer. Each user receives a role, and each route lists the roles it accepts. Think of an office building: a visitor badge opens the lobby, an employee badge opens the floors, and a manager badge opens the server room.

Two Questions Every API Asks

Authentication   Who are you?            ──▶  failure returns 401 Unauthorized
Authorization    What may you do here?   ──▶  failure returns 403 Forbidden

A 401 means the server does not know the user. A 403 means the server knows the user and refuses the action.

Designing Roles

Action              viewer   editor   admin
─────────────────   ──────   ──────   ─────
Read articles         yes      yes      yes
Create articles       no       yes      yes
Edit any article      no       yes      yes
Delete articles       no       no       yes
Manage users          no       no       yes

Keep the list short. Three or four roles cover most applications.

Defining Roles in Code

An enumeration prevents typos in role names.

from enum import Enum

class Role(str, Enum):
    viewer = "viewer"
    editor = "editor"
    admin = "admin"

The user model carries one role.

from pydantic import BaseModel

class User(BaseModel):
    username: str
    role: Role = Role.viewer

Getting the Current User

The earlier topics on JWT tokens and protected routes build a get_current_user dependency. This example reuses that function, and the sample below fakes it for clarity.

from fastapi import Depends, Header

def get_current_user(x_user: str = Header()):
    fake_users = {
        "asha": User(username="asha", role=Role.admin),
        "ravi": User(username="ravi", role=Role.editor),
        "mina": User(username="mina", role=Role.viewer),
    }
    return fake_users[x_user]

A Reusable Role Checker

A function that returns a dependency keeps role checks short and readable.

from fastapi import HTTPException

def require_roles(*allowed: Role):
    def checker(user: User = Depends(get_current_user)):
        if user.role not in allowed:
            raise HTTPException(status_code=403, detail="You do not have permission")
        return user
    return checker

Calling require_roles(Role.admin) builds a checker that lets only admins through.

Protecting Routes

from fastapi import FastAPI

app = FastAPI()

@app.get("/articles")
def read_articles(user: User = Depends(get_current_user)):
    return {"reader": user.username}

@app.post("/articles")
def create_article(user: User = Depends(require_roles(Role.editor, Role.admin))):
    return {"message": f"Article created by {user.username}"}

@app.delete("/articles/{article_id}")
def delete_article(article_id: int, user: User = Depends(require_roles(Role.admin))):
    return {"message": f"Article {article_id} deleted"}
Request                          Role      Result
──────────────────────────────   ───────   ───────────────
GET    /articles                 viewer    200 OK
POST   /articles                 viewer    403 Forbidden
POST   /articles                 editor    200 OK
DELETE /articles/4               editor    403 Forbidden
DELETE /articles/4               admin     200 OK

Storing Roles in JWT Tokens

Add the role to the token payload at login. The server reads the role from the verified token and skips a database lookup on each request.

token_data = {"sub": user.username, "role": user.role.value}

Tokens stay valid until they expire, so a changed role takes effect after the next login. Keep token lifetimes short when roles change often.

Checking Ownership

Roles alone miss one common rule: users may edit their own records. Combine the role check with an ownership check inside the route.

if article.author != user.username and user.role != Role.admin:
    raise HTTPException(status_code=403, detail="Not your article")

Security Habits

  • Enforce every rule on the server. Hiding a button in the frontend protects nothing.
  • Grant the smallest role that gets the job done.
  • Default new accounts to the lowest role.
  • Log denied attempts for later review.

A Short Walkthrough

Follow one request through the system. Ravi, an editor, sends a delete request. The server reads his token and builds a user object with the editor role. The route's dependency checks the allowed list, which names only admins. The check fails, and the server answers with a 403 response before any delete code runs.

The design keeps rules close to the routes. A reader who opens the route file sees the permitted roles right in the function signature. Auditors and new teammates find the policy without hunting through scattered if statements.

Review roles regularly. Teams grow, features change, and a role that made sense a year ago may hold too much power today.

Key Points

  • Authentication identifies users; authorization limits their actions.
  • Roles group permissions, and enums keep role names consistent.
  • A dependency factory such as require_roles guards routes in one line.
  • Return 401 for unknown users and 403 for forbidden actions.
  • Combine roles with ownership checks for personal records.