Authentication and Authorization in FastAPI

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

bash
poetry add "python-jose[cryptography]" passlib[bcrypt]
  • python-jose → JWT encoding/decoding.
  • passlib[bcrypt] → password hashing.

4.2 Create User & Auth Utilities

fastapi_todo/auth.py
1from datetime import datetime, timedelta
2from typing import Optional
3
4from jose import JWTError, jwt
5from passlib.context import CryptContext
6
7# Secret key & algorithm
8SECRET_KEY = "super-secret-key"
9ALGORITHM = "HS256"
10ACCESS_TOKEN_EXPIRE_MINUTES = 30
11
12# Password hashing context
13pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
14
15def 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)
18
19def get_password_hash(password: str) -> str:
20 """Hash a plain password."""
21 return pwd_context.hash(password)
22
23def 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)
29
30def 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.py
1from fastapi import Depends, HTTPException, status
2from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3from fastapi_todo.auth import (
4 create_access_token,
5 decode_access_token,
6 get_password_hash,
7 verify_password,
8)
9
10# OAuth2 scheme
11oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
12
13# Fake user DB
14fake_users_db = {
15 "alice": {
16 "username": "alice",
17 "hashed_password": get_password_hash("wonderland"),
18 }
19}
20
21
22@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")
28
29 access_token = create_access_token({"sub": form_data.username})
30 return {"access_token": access_token, "token_type": "bearer"}

Explanation:

  • OAuth2PasswordBearer handles Authorization: Bearer <token> headers.
  • The /login route 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.py
1def 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 username
9 except Exception:
10 raise HTTPException(status_code=401, detail="Invalid or expired token")
11
12@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_user decodes JWT and validates it.
  • If valid, the username is returned.
  • /secure-todos requires a valid token to respond.

4.5: Tests for Authentication

tests/test_auth.py
1from fastapi.testclient import TestClient
2from fastapi_todo.main import app
3
4client = TestClient(app)
5
6
7def test_login_and_secure_access() -> None:
8 # Login to get a token
9 response = client.post("/login", data={"username": "alice", "password": "wonderland"})
10 assert response.status_code == 200
11 token = response.json()["access_token"]
12
13 # Use token to access secure route
14 headers = {"Authorization": f"Bearer {token}"}
15 response = client.get("/secure-todos", headers=headers)
16 assert response.status_code == 200
17 assert isinstance(response.json(), list)
18
19
20def test_invalid_login() -> None:
21 response = client.post("/login", data={"username": "alice", "password": "wrong"})
22 assert response.status_code == 401
23
24
25def 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:

bash
poetry 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

Feedback