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
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 <token></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
OAuth2PasswordBearer— a FastAPI dependency that readsAuthorization: Bearer <token>from the request./tokenendpoint — accepts form-data (username,password), returns{access_token, token_type}.OAuth2PasswordRequestForm— parses that form-data for us.get_current_userdependency — 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.
OAuth2PasswordBearerproduces theAuthorizebutton in/docsfor 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 ofDepends().