OAuth2 Password Flow

advanced fastapi auth oauth2 security

OAuth2 sounds scary. In simple language: it’s just a standard way to say “send me your username and password to a specific URL, and I’ll give you a token. Then send that token on every future request.” FastAPI bakes this in via OAuth2PasswordBearer.

It’s the default auth pattern in FastAPI tutorials and most real apps. SwaggerUI even gets a working “Authorize” button for free.

The flow

OAuth2 Password Flow
Client
POST /token
username+password
Server
  <div style="border: 1px solid var(--color-border); padding: 8px; border-radius: 4px; text-align: center;">Client</div>
  <div style="text-align: center; color: var(--color-tag-beginner);">← access_token (JWT)</div>
  <div style="border: 1px solid var(--color-border); padding: 8px; border-radius: 4px; text-align: center;">Server</div>

  <div style="border: 1px solid var(--color-border); padding: 8px; border-radius: 4px; text-align: center;">Client</div>
  <div style="text-align: center; color: var(--color-accent);">GET /users/me<br/>Authorization: Bearer &lt;token&gt;</div>
  <div style="border: 1px solid var(--color-border); padding: 8px; border-radius: 4px; text-align: center;">Server</div>

  <div style="border: 1px solid var(--color-border); padding: 8px; border-radius: 4px; text-align: center;">Client</div>
  <div style="text-align: center; color: var(--color-tag-beginner);">← user data</div>
  <div style="border: 1px solid var(--color-border); padding: 8px; border-radius: 4px; text-align: center;">Server</div>
</div>

The pieces

  1. OAuth2PasswordBearer — a FastAPI dependency that reads Authorization: Bearer <token> from the request.
  2. /token endpoint — accepts form-data (username, password), returns {access_token, token_type}.
  3. OAuth2PasswordRequestForm — parses that form-data for us.
  4. get_current_user dependency — decodes the token, loads the user, gates routes.

Full working example

from datetime import datetime, timedelta, timezone
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt, JWTError
from passlib.context import CryptContext
from pydantic import BaseModel

SECRET_KEY = "swap-this-with-a-real-secret"
ALGORITHM = "HS256"
TOKEN_EXPIRE_MIN = 30

app = FastAPI()
pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# pretend DB
fake_db = {
    "manish": {
        "username": "manish",
        "hashed_password": pwd_ctx.hash("secret"),
    }
}

class Token(BaseModel):
    access_token: str
    token_type: str

def authenticate(username: str, password: str):
    user = fake_db.get(username)
    if not user or not pwd_ctx.verify(password, user["hashed_password"]):
        return None
    return user

def make_token(data: dict) -> str:
    payload = data.copy()
    payload["exp"] = datetime.now(timezone.utc) + timedelta(minutes=TOKEN_EXPIRE_MIN)
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

@app.post("/token", response_model=Token)
def login(form: OAuth2PasswordRequestForm = Depends()):
    user = authenticate(form.username, form.password)
    if not user:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "bad credentials")
    return {"access_token": make_token({"sub": user["username"]}), "token_type": "bearer"}

def get_current_user(token: str = Depends(oauth2_scheme)):
    cred_err = HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid token")
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
    except JWTError:
        raise cred_err
    user = fake_db.get(username)
    if not user:
        raise cred_err
    return user

@app.get("/users/me")
def me(user = Depends(get_current_user)):
    return {"username": user["username"]}

Hitting it

# get a token
curl -X POST http://localhost:8000/token \
  -d "username=manish&password=secret"

# use it
curl http://localhost:8000/users/me \
  -H "Authorization: Bearer eyJhbGc..."

Scopes — fine-grained permissions

Scopes are labels attached to a token that say what it’s allowed to do (read:items, admin, etc.). We declare them on the scheme and check them in dependencies via SecurityScopes.

oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="token",
    scopes={"read:items": "Read items", "admin": "Full access"},
)

@app.get("/items")
def list_items(user = Security(get_current_user, scopes=["read:items"])):
    ...

In simple language: scopes are like roles, but bundled into the token itself instead of a DB lookup on every request.

Why this is the standard FastAPI pattern

  • It’s a real spec (OAuth2 RFC 6749), so any client library understands it.
  • OAuth2PasswordBearer produces the Authorize button in /docs for free.
  • It’s flexible: swap JWT for opaque tokens, add refresh tokens, layer in scopes — same skeleton.

Interview cheat sheet

  • Password flow = user trades password for token at /token. Client uses token on every subsequent call.
  • OAuth2PasswordBearer(tokenUrl="token") — declares the scheme, reads the header.
  • OAuth2PasswordRequestForm — parses the form-encoded login body (spec-mandated).
  • Hash passwords with bcrypt (passlib). Never store plaintext.
  • Tokens are usually JWTs but don’t have to be.
  • Scopes for permission gating; combine with Security() instead of Depends().