Let’s Start With a Real Problem
Open Swagger UI for the projects you built in Chapters 14 and 15. Look in the top-right corner. There’s no padlock. No “Authorize” button. Every single time you want to test a protected route, you have to manually copy a token, paste it into a header field, and repeat that for every request. That’s not how FastAPI’s own documentation demos work — and there’s a reason for that.
FastAPI has an official, built-in security flow — OAuth2PasswordBearer — that Swagger UI recognizes automatically. The moment you use it correctly, that padlock appears, and you can log in once and test every protected endpoint without touching headers manually again. This chapter rebuilds our authentication system using that exact official pattern, and then goes further — layering multiple levels of route protection, like “must be logged in” and “must be an admin.”
What You’ll Learn
- What
OAuth2PasswordBeareris, and why it’s the pattern FastAPI’s own docs are built around - How
OAuth2PasswordRequestFormpowers Swagger UI’s built-in “Authorize” button - How to rebuild login and route protection using this official flow
- How to layer dependencies to build multiple levels of access control (logged in → active → admin)
- Theory + hands-on assignment questions to lock in what you’ve learned
💡 Where this fits in: Chapter 14 issued tokens. Chapter 15 verified them using
HTTPBearer. This chapter swaps that verification mechanism for FastAPI’s officialOAuth2PasswordBearer, and adds role-based route protection on top. The underlying JWT concepts are the same ones you already know — what’s new here is the official plumbing and the layered security pattern.
Theory: What OAuth2PasswordBearer Actually Does
OAuth2PasswordBearer doesn’t perform authentication itself — its real job is telling FastAPI (and Swagger UI) where the login endpoint is, so the interactive docs can build a proper login form for you automatically.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
That tokenUrl="auth/login" is what makes the padlock icon appear next to every protected route in /docs. Click it, and Swagger UI shows a real username/password form — exactly like a real client app would present to a user.
For this to work, your login endpoint needs to accept data in a specific shape: OAuth2 spec-compliant form data (username and password fields), not a JSON body like we used in Chapters 14 and 15. FastAPI provides OAuth2PasswordRequestForm specifically for this:
def login(form_data: OAuth2PasswordRequestForm = Depends()):
# form_data.username and form_data.password are available here
💡 A quick technical note: Parsing form data requires the
python-multipartpackage. If you installed FastAPI withuv add fastapi --extra standard(as we have throughout this series), it’s already included — no extra install step needed.
Program 1: The Official OAuth2PasswordBearer Login Flow
Program Objective
Build a small, standalone FastAPI project using FastAPI’s official OAuth2PasswordBearer + OAuth2PasswordRequestForm pattern — complete with real password hashing — so that Swagger UI’s “Authorize” button works out of the box.
Step-by-Step Implementation
Step 1: Create the project
uv init oauth2-official-demo
cd oauth2-official-demo
uv add fastapi --extra standard
uv add pydantic-settings
uv add sqlalchemy
uv add "passlib[bcrypt]"
uv add "python-jose[cryptography]"
Step 2: Create the project structure
mkdir -p app/core app/db app/models app/schemas app/services app/routers
touch app/__init__.py app/core/__init__.py app/db/__init__.py app/models/__init__.py app/schemas/__init__.py app/services/__init__.py app/routers/__init__.py
oauth2-official-demo/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── security.py
│ │ └── dependencies.py
│ ├── db/
│ │ ├── __init__.py
│ │ └── database.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── token.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── user_service.py
│ └── routers/
│ ├── __init__.py
│ ├── users.py
│ └── auth.py
├── pyproject.toml
└── uv.lock
Step 3: Create each file with the code below. Step 4: Run and test.
Complete Working Source Code
app/core/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "OAuth2 Official Demo"
app_version: str = "1.0.0"
database_url: str = "sqlite:///./app.db"
# NOTE: Hardcoded placeholder secret for learning purposes only.
# Chapter 17 covers moving secrets like this into a proper .env file.
jwt_secret_key: str = "a-very-insecure-demo-secret-change-this"
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 30
settings = Settings()
app/core/security.py
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain_password: str) -> str:
return pwd_context.hash(plain_password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
to_encode = {"sub": subject, "exp": expire}
return jwt.encode(to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
def decode_token(token: str) -> dict:
try:
return jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
except JWTError as exc:
raise ValueError("Invalid or expired token.") from exc
app/core/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.core.security import decode_token
from app.db.database import get_db
from app.models.user import User
from app.services.user_service import get_user_by_username
# tokenUrl points Swagger UI at our login endpoint, enabling the "Authorize" button.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db),
) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials.",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = decode_token(token)
except ValueError:
raise credentials_exception
username = payload.get("sub")
if username is None:
raise credentials_exception
user = get_user_by_username(db, username)
if user is None:
raise credentials_exception
return user
app/db/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from app.core.config import settings
engine = create_engine(settings.database_url, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app/models/user.py
from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
username: Mapped[str] = mapped_column(String(30), unique=True, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
app/schemas/user.py
from typing import Annotated
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class UserCreate(BaseModel):
username: Annotated[str, Field(min_length=3, max_length=30)]
email: Annotated[EmailStr, Field()]
password: Annotated[str, Field(min_length=8, max_length=64)]
class UserOut(BaseModel):
id: int
username: str
email: EmailStr
is_active: bool
model_config = ConfigDict(from_attributes=True)
app/schemas/token.py
from pydantic import BaseModel
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
app/services/user_service.py
from sqlalchemy.orm import Session
from app.core.security import hash_password
from app.models.user import User
from app.schemas.user import UserCreate
def create_user(db: Session, user_in: UserCreate) -> User:
new_user = User(
username=user_in.username,
email=user_in.email,
hashed_password=hash_password(user_in.password),
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
def get_user_by_username(db: Session, username: str) -> User | None:
return db.query(User).filter(User.username == username).first()
app/routers/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.security import create_access_token, verify_password
from app.db.database import get_db
from app.schemas.token import TokenResponse
from app.services.user_service import get_user_by_username
router = APIRouter(prefix="/auth", tags=["Auth"])
@router.post("/login", response_model=TokenResponse)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = get_user_by_username(db, form_data.username)
if user is None or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password.",
)
access_token = create_access_token(subject=user.username)
return TokenResponse(access_token=access_token)
app/routers/users.py
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app.core.dependencies import get_current_user
from app.db.database import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserOut
from app.services.user_service import create_user
router = APIRouter(prefix="/users", tags=["Users"])
@router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register_user(user_in: UserCreate, db: Session = Depends(get_db)):
return create_user(db, user_in)
@router.get("/me", response_model=UserOut)
def read_current_user(current_user: User = Depends(get_current_user)):
return current_user
app/main.py
from fastapi import FastAPI
from app.core.config import settings
from app.db.database import Base, engine
from app.models import user # noqa: F401
from app.routers import auth, users
Base.metadata.create_all(bind=engine)
app = FastAPI(title=settings.app_name, version=settings.app_version)
app.include_router(users.router)
app.include_router(auth.router)
@app.get("/", tags=["Root"])
def read_root():
return {"message": f"Welcome to {settings.app_name}"}
Detailed Code Explanation
OAuth2PasswordBearer(tokenUrl="auth/login")replaces Chapter 15’sHTTPBearer(). Functionally, it still extracts aBearertoken from theAuthorizationheader — but because it also carries thetokenUrl, FastAPI can generate the correct OpenAPI security scheme, which is exactly what makes Swagger UI render its “Authorize” button and know which endpoint to call when you log in through it.OAuth2PasswordRequestForm = Depends()in the login route replaces our earlierLoginRequestJSON schema. FastAPI automatically expectsapplication/x-www-form-urlencodeddata withusernameandpasswordfields — the exact shape the OAuth2 password flow specifies, and the exact shape Swagger UI’s “Authorize” popup submits when you type your credentials into it.- Everything else —
verify_password,create_access_token,get_current_user‘s core logic — is functionally identical to Chapter 15. The mechanism for extracting the token changed; the verification logic itself didn’t need to. get_current_userstill returns a realUserobject looked up from the database, keeping the same safety guarantee from Chapter 15: a deleted user’s old token stops working immediately, even if it hasn’t technically expired.
Execution & Testing
uv run fastapi dev app/main.py
- Open
http://127.0.0.1:8000/docs. Register a user viaPOST /users/register. - Look at the top-right corner of the page — you should now see a green “Authorize” button with a padlock icon. Click it.
- In the popup, enter your username and password (leave
client_id,client_secret, and scopes blank), then click Authorize, then Close. - Notice the padlock icons next to every protected route are now shown as “locked” (authorized). Call
GET /users/medirectly — you won’t need to paste any token manually. Swagger UI now automatically attaches your token to every request for the rest of your session. - Click Authorize again and then Logout to clear the session, then try
GET /users/meagain — confirm it now fails with401, proving the automatic authorization really was doing the work.
Program 2: Layered Secure Routes (User → Active User → Admin)
Program Objective
Extend the project with role-based access control — building a chain of dependencies where a route can require “must be logged in,” “must be logged in and active,” or “must be logged in, active, and an admin,” each layer building on the one before it.
Step-by-Step Implementation
Step 1: Create the project
uv init layered-security-demo
cd layered-security-demo
uv add fastapi --extra standard
uv add pydantic-settings
uv add sqlalchemy
uv add "passlib[bcrypt]"
uv add "python-jose[cryptography]"
Step 2: Create the project structure
mkdir -p app/core app/db app/models app/schemas app/services app/routers
touch app/__init__.py app/core/__init__.py app/db/__init__.py app/models/__init__.py app/schemas/__init__.py app/services/__init__.py app/routers/__init__.py
layered-security-demo/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── security.py
│ │ └── dependencies.py
│ ├── db/
│ │ ├── __init__.py
│ │ └── database.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── token.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── user_service.py
│ └── routers/
│ ├── __init__.py
│ ├── users.py
│ ├── auth.py
│ └── admin.py
├── pyproject.toml
└── uv.lock
Step 3: Create each file with the code below. Step 4: Run and test.
Complete Working Source Code
app/core/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Layered Security Demo"
app_version: str = "1.0.0"
database_url: str = "sqlite:///./app.db"
jwt_secret_key: str = "a-very-insecure-demo-secret-change-this"
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 30
settings = Settings()
app/core/security.py
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain_password: str) -> str:
return pwd_context.hash(plain_password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
to_encode = {"sub": subject, "exp": expire}
return jwt.encode(to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
def decode_token(token: str) -> dict:
try:
return jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
except JWTError as exc:
raise ValueError("Invalid or expired token.") from exc
app/core/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.core.security import decode_token
from app.db.database import get_db
from app.models.user import User
from app.services.user_service import get_user_by_username
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db),
) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials.",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = decode_token(token)
except ValueError:
raise credentials_exception
username = payload.get("sub")
if username is None:
raise credentials_exception
user = get_user_by_username(db, username)
if user is None:
raise credentials_exception
return user
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="This account has been deactivated.",
)
return current_user
def require_admin(current_user: User = Depends(get_current_active_user)) -> User:
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="This action requires admin privileges.",
)
return current_user
app/db/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from app.core.config import settings
engine = create_engine(settings.database_url, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app/models/user.py
from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
username: Mapped[str] = mapped_column(String(30), unique=True, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
app/schemas/user.py
from typing import Annotated
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class UserCreate(BaseModel):
username: Annotated[str, Field(min_length=3, max_length=30)]
email: Annotated[EmailStr, Field()]
password: Annotated[str, Field(min_length=8, max_length=64)]
class UserOut(BaseModel):
id: int
username: str
email: EmailStr
is_active: bool
is_admin: bool
model_config = ConfigDict(from_attributes=True)
app/schemas/token.py
from pydantic import BaseModel
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
app/services/user_service.py
from sqlalchemy.orm import Session
from app.core.security import hash_password
from app.models.user import User
from app.schemas.user import UserCreate
def create_user(db: Session, user_in: UserCreate, is_admin: bool = False) -> User:
new_user = User(
username=user_in.username,
email=user_in.email,
hashed_password=hash_password(user_in.password),
is_admin=is_admin,
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
def get_user_by_username(db: Session, username: str) -> User | None:
return db.query(User).filter(User.username == username).first()
app/routers/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.security import create_access_token, verify_password
from app.db.database import get_db
from app.schemas.token import TokenResponse
from app.services.user_service import get_user_by_username
router = APIRouter(prefix="/auth", tags=["Auth"])
@router.post("/login", response_model=TokenResponse)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = get_user_by_username(db, form_data.username)
if user is None or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password.",
)
access_token = create_access_token(subject=user.username)
return TokenResponse(access_token=access_token)
app/routers/users.py
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app.core.dependencies import get_current_active_user
from app.db.database import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserOut
from app.services.user_service import create_user
router = APIRouter(prefix="/users", tags=["Users"])
@router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register_user(user_in: UserCreate, db: Session = Depends(get_db)):
return create_user(db, user_in)
@router.get("/me", response_model=UserOut)
def read_current_user(current_user: User = Depends(get_current_active_user)):
return current_user
app/routers/admin.py
from fastapi import APIRouter, Depends
from app.core.dependencies import require_admin
from app.models.user import User
router = APIRouter(prefix="/admin", tags=["Admin"], dependencies=[Depends(require_admin)])
@router.get("/stats")
def get_admin_stats():
return {"total_users": 128, "total_products": 42}
@router.get("/whoami")
def admin_whoami(current_user: User = Depends(require_admin)):
return {"username": current_user.username, "role": "admin"}
app/main.py
from fastapi import FastAPI
from app.core.config import settings
from app.db.database import Base, engine
from app.models import user # noqa: F401
from app.routers import admin, auth, users
Base.metadata.create_all(bind=engine)
app = FastAPI(title=settings.app_name, version=settings.app_version)
app.include_router(users.router)
app.include_router(auth.router)
app.include_router(admin.router)
@app.get("/", tags=["Root"])
def read_root():
return {"message": f"Welcome to {settings.app_name}"}
Detailed Code Explanation
Usermodel gains a second flag,is_admin, alongside the existingis_active.core/dependencies.pynow defines three dependencies that build on each other, each one deeper than the last:get_current_user— the base layer: is this a valid, known user at all?get_current_active_user(current_user: User = Depends(get_current_user))— takes a dependency on another dependency. FastAPI resolvesget_current_userfirst, then passes its result in here, where we add one more check: is this account active?require_admin(current_user: User = Depends(get_current_active_user))— builds on that, adding the final check: is this user an admin?
- This is called dependency chaining, and it’s a genuinely powerful FastAPI pattern: each layer only needs to worry about its own single responsibility, and FastAPI automatically resolves the whole chain in the correct order for you.
routers/admin.pydemonstrates two ways to apply this:dependencies=[Depends(require_admin)]at the router level protects every route in the file with zero repetition (the pattern from Chapter 12), whileadmin_whoamialso injectscurrent_user: User = Depends(require_admin)directly into its parameters, for routes that need to actually use the current user’s data, not just gate access to it.user_service.create_user(..., is_admin: bool = False)defaults every new registration to a regular, non-admin account — a sensible, secure default. There’s no public endpoint for granting admin status in this demo; in a real project, that would be a carefully guarded action, often performed directly in a database console or by another already-verified admin.
Execution & Testing
uv run fastapi dev app/main.py
- Register a regular user via
POST /users/register, then log in and click Authorize in Swagger UI, exactly as in Program 1. - Call
GET /users/me— confirm it works, and noteis_admin: falsein the response. - Call
GET /admin/stats— confirm you get a403 Forbiddenwith"This action requires admin privileges.", even though you’re fully logged in. - Promote yourself to admin directly in the database (for this learning exercise only): run
sqlite3 app.db "UPDATE users SET is_admin = 1 WHERE username = 'your_username';"in your terminal. - In Swagger UI, click Authorize → Logout, then log in again (tokens don’t automatically refresh their contents, so you need a fresh login to pick up the change — though in this simple version the check happens against the live database record anyway).
- Call
GET /admin/statsagain — confirm you now get a200with the stats data.
Tips & Instructions for Developers
- ✅ Use
OAuth2PasswordBearer+OAuth2PasswordRequestFormfor any project where you want Swagger UI’s “Authorize” button to work — it’s a small change with a big developer-experience payoff. - ✅ Chain dependencies for layered access control (
get_current_user→get_current_active_user→require_admin) instead of writing repeatedifchecks inside every route. - ✅ Default new accounts to the least privileged role possible (
is_admin=False) — grant elevated access explicitly and deliberately, never by default. - ✅ Use
403 Forbidden(not401 Unauthorized) when a user is correctly authenticated but lacks permission for a specific action — the two mean different things, and mixing them up confuses API consumers. - ✅ Keep each dependency layer focused on exactly one check — it keeps the chain easy to read, test, and reorder if your permission model grows more complex later.
- ❌ Don’t build a public API endpoint that lets any user grant themselves (or others) admin rights — that kind of action needs much stronger safeguards than a simple
PATCHrequest. - ❌ Don’t skip testing the “should be denied” paths — a permissions system that’s never tested against an unauthorized user is a permissions system you can’t actually trust.
Official FastAPI Documentation References
- Security — OAuth2 with Password (and hashing), Bearer with JWT tokens
- Security — Simple OAuth2 with Password and Bearer
- Security — Get Current User
- Dependencies — Sub-dependencies
- Dependencies in Path Operation Decorators
Theory Questions
- What is the actual purpose of the
tokenUrlparameter inOAuth2PasswordBearer, given that token verification itself doesn’t call that URL? - Why does
OAuth2PasswordRequestFormexpect form data instead of a JSON body? - What is “dependency chaining,” and what problem does it solve compared to writing repeated permission checks in every route?
- Why is
403 Forbiddenthe more accurate status code than401 Unauthorizedfor an authenticated user who lacks the right permissions? - Why does
create_userdefaultis_admintoFalse, and why is there no API endpoint in this demo for granting admin rights?
Practical Assignment
- Add a third tier: Starting from Program 2’s project, add a
is_superadminflag and arequire_superadmindependency that builds onrequire_admin, following the same chaining pattern. - Protect a single route, not a whole router: Starting from Program 2’s project, add a new
DELETE /users/{user_id}endpoint to theusersrouter that requiresrequire_admindirectly on that one route (not the whole router), whileGET /users/mecontinues to only requireget_current_active_user. - Write the chain from memory: Without looking back at the code, write out (in comments or on paper) the full dependency chain from
require_admindown to the raw incoming request — naming every dependency involved and what single check each one adds.
Summary
In this chapter, you learned:
- How
OAuth2PasswordBearerandOAuth2PasswordRequestFormpower Swagger UI’s official “Authorize” flow - How to rebuild login and token verification using FastAPI’s official, spec-compliant pattern
- How to chain dependencies to build layered access control — logged in, active, and admin-only routes
- Why
403and401mean genuinely different things, and when to use each
