Authentication and JWT

Introduction

API clients authenticate with JWT access tokens sent in the Authorization: Bearer header. FastAPI documents the OAuth2 password flow pattern: login with username/password, receive a token, pass token to protected routes. This chapter implements user registration, login, password hashing, and Depends(get_current_user).

Prerequisites

Security Module

app/core/security.py:

Code explanation:

  • sub (subject) stores user id as string in JWT
  • secret_key from settings signs tokens—must be strong in production

Compare Spring Boot JWT and Flask JWT.

User Model and Schemas

app/models/user.py:

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, index=True)
    hashed_password: Mapped[str] = mapped_column(String(255))

app/schemas/user.py:

OAuth2 Bearer Dependency

app/api/deps.py:

Code explanation:

  • tokenUrl must match login route path for Swagger Authorize button
  • OAuth2PasswordBearer extracts Bearer token from header

Auth Routes

app/api/v1/endpoints/auth.py:

Add import:

python
from fastapi.security import OAuth2PasswordRequestForm

Code explanation:

  • Login uses form fields username and password (OAuth2 spec)—not JSON body
  • Swagger Authorize sends same format

JSON login alternative (optional second endpoint):

python
class LoginJSON(BaseModel):
    username: str
    password: str
 
@router.post("/login/json", response_model=Token)
def login_json(body: LoginJSON, db: Session = Depends(get_db)):
    ...

Protected Route

python
from app.api.deps import get_current_user
 
@router.get("/me", response_model=UserRead)
def read_me(current_user: User = Depends(get_current_user)):
    return current_user

Register router in api.py.

Test With curl

Register:

bash
curl -X POST http://127.0.0.1:8000/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"secret123"}'

Login (form):

bash
TOKEN=$(curl -s -X POST http://127.0.0.1:8000/api/v1/auth/login \
  -d "username=alice&password=secret123" | jq -r .access_token)

Protected:

bash
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/api/v1/auth/me

Swagger Authorize

Open /docsAuthorize → enter username/password (OAuth2 flow) or paste Bearer <token> depending on UI mode.

Warning

Never Return hashed_password

Use UserRead without password field in all responses.

Refresh Tokens (Concept)

Short access token + long refresh token improves security. Store refresh tokens server-side or in HTTP-only cookies—full implementation is optional stretch goal.

FAQ

401 on protected route?

Missing header, expired token, or wrong SECRET_KEY after restart.

Login expects form not JSON?

Use OAuth2PasswordRequestForm or add JSON login endpoint.

bcrypt version warnings?

Pin passlib and bcrypt compatible versions in requirements.txt.

PyJWT instead of python-jose?

Both work—adjust encode/decode API accordingly.

Token in query string?

Avoid—logs and referrers leak tokens.

HS256 vs RS256?

HS256 + shared secret is fine for monolith; RS256 for multi-service verification.