
Chapter Outline
Chapter 4: Authentication and Authorization in FastAPI
In this chapter, you will learn:
- The difference between authentication (who you are) and authorization (what you can do).
- How to implement JWT (JSON Web Token)-based authentication in FastAPI.
- How to protect routes with authentication.
- How to test secure endpoints using Pytest.
In FastAPI, both are usually implemented using dependencies that run before the route handler.
We’ll add a user system to our Todo API:
POST /login→ returns a JWT token.- Protected routes (
/secure-todos) → require valid JWT.
4.1 Install Dependencies
bashpoetry add "python-jose[cryptography]" passlib[bcrypt]
- python-jose → JWT encoding/decoding.
- passlib[bcrypt] → password hashing.
4.2 Create User & Auth Utilities
fastapi_todo/auth.py1from datetime import datetime, timedelta2from typing import Optional34from jose import JWTError, jwt5from passlib.context import CryptContext67# Secret key & algorithm8SECRET_KEY = "super-secret-key"9ALGORITHM = "HS256"10ACCESS_TOKEN_EXPIRE_MINUTES = 301112# Password hashing context13pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")1415def verify_password(plain_password: str, hashed_password: str) -> bool:16 """Verify a plain password against a hashed password."""17 return pwd_context.verify(plain_password, hashed_password)1819def get_password_hash(password: str) -> str:20 """Hash a plain password."""21 return pwd_context.hash(password)2223def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:24 """Generate a JWT access token."""25 to_encode = data.copy()26 expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))27 to_encode.update({"exp": expire})28 return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)2930def decode_access_token(token: str) -> dict:31 """Decode a JWT token and return its payload."""32 try:33 return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])34 except JWTError:35 raise ValueError("Invalid token")
Explanation:
- Passwords are hashed with bcrypt.
- JWTs contain user identity and expiration time.
- Invalid tokens raise an error.
4.3 User Model and Login Endpoint
fastapi_todo/main.py1from fastapi import Depends, HTTPException, status2from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm3from fastapi_todo.auth import (4 create_access_token,5 decode_access_token,6 get_password_hash,7 verify_password,8)910# OAuth2 scheme11oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")1213# Fake user DB14fake_users_db = {15 "alice": {16 "username": "alice",17 "hashed_password": get_password_hash("wonderland"),18 }19}202122@app.post("/login")23def login(form_data: OAuth2PasswordRequestForm = Depends()) -> dict[str, str]:24 """Authenticate user and return JWT token."""25 user = fake_users_db.get(form_data.username)26 if not user or not verify_password(form_data.password, user["hashed_password"]):27 raise HTTPException(status_code=401, detail="Invalid username or password")2829 access_token = create_access_token({"sub": form_data.username})30 return {"access_token": access_token, "token_type": "bearer"}
Explanation:
OAuth2PasswordBearerhandlesAuthorization: Bearer <token>headers.- The
/loginroute checks user credentials and issues a JWT token. - For now, we use a fake in-memory user store.
4.4 Protect Routes with JWT
fastapi_todo/main.py1def get_current_user(token: str = Depends(oauth2_scheme)) -> str:2 """Extract current user from JWT token."""3 try:4 payload = decode_access_token(token)5 username = payload.get("sub")6 if not username:7 raise HTTPException(status_code=401, detail="Invalid token payload")8 return username9 except Exception:10 raise HTTPException(status_code=401, detail="Invalid or expired token")1112@app.get("/secure-todos")13def secure_list_todos(current_user: str = Depends(get_current_user)) -> list[Todo]:14 """Return todos for authenticated users only."""15 return todos
Explanation:
get_current_userdecodes JWT and validates it.- If valid, the username is returned.
/secure-todosrequires a valid token to respond.
4.5: Tests for Authentication
tests/test_auth.py1from fastapi.testclient import TestClient2from fastapi_todo.main import app34client = TestClient(app)567def test_login_and_secure_access() -> None:8 # Login to get a token9 response = client.post("/login", data={"username": "alice", "password": "wonderland"})10 assert response.status_code == 20011 token = response.json()["access_token"]1213 # Use token to access secure route14 headers = {"Authorization": f"Bearer {token}"}15 response = client.get("/secure-todos", headers=headers)16 assert response.status_code == 20017 assert isinstance(response.json(), list)181920def test_invalid_login() -> None:21 response = client.post("/login", data={"username": "alice", "password": "wrong"})22 assert response.status_code == 401232425def test_secure_route_without_token() -> None:26 response = client.get("/secure-todos")27 assert response.status_code == 401
Explanation:
- Logs in, retrieves token, and uses it for authenticated requests.
- Tests happy path and failure cases (wrong password, missing token).
Run with:
bashpoetry run pytest
4.6 JWT Authentication Flow
sequenceDiagram
participant C as Client
participant S as FastAPI Server
C->>S: POST /login (username, password)
S-->>C: JWT Token
C->>S: GET /secure-todos (Authorization: Bearer token)
S->>S: Decode & Validate JWT
S-->>C: Response (Todos)
4.7 Further Reading
Check your understanding
Test your knowledge of Authentication and Authorization in FastAPI