Complete DevOps Bootcamp: Master DevOps in 12 Weeks
FastAPIAuthentication and Authorization

OAuth2 Password Flow

OAuth2 is a large specification. Most of it is about delegated access - "let this third-party app act on your behalf." For a backend that owns its own users and just needs to authenticate them with a username and password, you only need a small slice of OAuth2 called the password flow.

FastAPI ships with built-in support for it, which is why you see OAuth2PasswordBearer and OAuth2PasswordRequestForm in almost every FastAPI tutorial. It is not that OAuth2 is required for login - it is that adopting its conventions makes your API speak the same language as the Swagger UI, the docs system, and a lot of client libraries.

The shape of the flow

Client                       Server
  |                            |
  |  POST /auth/token          |
  |  username=alice            |
  |  password=hunter2          |
  | -------------------------> |
  |                            |  verify credentials
  |                            |  build access_token
  |  { access_token, type }    |
  | <------------------------- |
  |                            |
  |  GET /me                   |
  |  Authorization: Bearer ... |
  | -------------------------> |
  |                            |  decode token, find user
  |  { id, email, ... }        |
  | <------------------------- |

Two endpoints. One issues tokens, the other reads them.

Why a special form, not JSON?

OAuth2 mandates that the token endpoint accept application/x-www-form-urlencoded, the same encoding HTML forms use. That decision predates JSON being everywhere. FastAPI follows the spec so that standard tools just work, which is why login uses OAuth2PasswordRequestForm instead of a Pydantic model.

You don't need to design that form yourself - the dependency unpacks the body for you.

The token endpoint

from datetime import datetime, timedelta, timezone

import jwt
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session

from .database import get_db
from .models import User
from .security import verify_password

SECRET_KEY = "change-me-and-keep-me-secret"
ALGORITHM = "HS256"
ACCESS_TOKEN_MINUTES = 30

router = APIRouter(prefix="/auth", tags=["auth"])

def create_access_token(subject: str) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_MINUTES)
    payload = {"sub": subject, "exp": expire}
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

@router.post("/token")
def login(
    form: OAuth2PasswordRequestForm = Depends(),
    db: Session = Depends(get_db),
):
    user = db.query(User).filter(User.email == form.username).first()
    if user is None or not verify_password(form.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    token = create_access_token(subject=user.email)
    return {"access_token": token, "token_type": "bearer"}

A few details worth pausing on:

  • form.username holds whatever was sent in the username field. We're storing emails as the identifier, so we look up by email but the field name stays as username because the spec says so.
  • The error message is intentionally vague. "Incorrect email or password" gives away less than "no such user" - an attacker should not be able to enumerate accounts by trying random emails.
  • The WWW-Authenticate header is part of the OAuth2 convention for 401 responses on a Bearer-protected endpoint.

The bearer scheme

For other endpoints to read the token, FastAPI provides a dependency that pulls it out of the Authorization: Bearer <token> header.

from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")

The tokenUrl is the path the docs UI should call to obtain a token when a developer clicks the "Authorize" button. It is not used at runtime for any redirection - it is purely metadata for Swagger.

The "current user" dependency

This is the piece almost every protected route depends on.

from jwt import InvalidTokenError

def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db),
) -> User:
    credentials_error = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        email = payload.get("sub")
        if email is None:
            raise credentials_error
    except InvalidTokenError:
        raise credentials_error

    user = db.query(User).filter(User.email == email).first()
    if user is None:
        raise credentials_error
    return user

Use it from any route that needs to know who is calling:

@app.get("/me", response_model=UserOut)
def read_me(current: User = Depends(get_current_user)):
    return current

Trying it from Swagger

Open /docs. You will see a small padlock icon on protected routes and an Authorize button at the top right. Click it, type a real email and password, and the docs page will send the password-flow request for you and remember the returned token. Every subsequent "Try it out" call will include the Authorization header automatically.

This is one of the genuinely lovely things about FastAPI. You get a working login UI for free.

Where the password flow stops being a good idea

The OAuth2 spec itself now discourages the password flow for third-party clients - apps you don't own should never see your users' passwords. The flow is still fine when:

SituationPassword flow OK?
Your own first-party frontend talking to your own backendYes
A mobile app that you also shipYes
A CLI tool used by your own teamYes
A random third-party integrationNo - use authorization code flow
Service-to-service trafficNo - use client credentials or API keys

If you ever find yourself building a public OAuth2 provider, you will want to graduate to the authorization code flow with PKCE. For an app where you control both ends, the password flow is honest and simple.

The handoff

We've built the issuing side and a basic decoder. The next doc digs into JWT specifically - why those payloads look the way they do, what exp and sub actually mean, and how refresh tokens fit in so users don't have to log in every thirty minutes.

How is this guide?

Last updated on