What OAuth2 “Password Flow” Means in FastAPI
FastAPI includes helpers for OAuth2 flows. For beginner-friendly APIs (especially internal tools or first-party apps), a common starting point is the OAuth2 Password flow: the client sends a username and password to a token endpoint, and the API returns an access token. The client then calls protected endpoints with Authorization: Bearer <token>.
In production, many teams later move to an external identity provider (Auth0, Keycloak, Cognito, etc.). The mechanics you learn here still apply: validate credentials, issue tokens, and protect routes.
JWT in One Minute
A JWT (JSON Web Token) is a signed string that contains claims (payload) like the user identifier and expiration time. The server signs it with a secret key (or private key). When a request arrives, the server verifies the signature and checks claims like exp (expiration).
- Stateless: you don’t need to store sessions server-side for basic access tokens.
- Not encrypted: JWT payload is base64-encoded, not secret. Do not put passwords or sensitive data inside.
Security Building Blocks
Password Hashing (Never Store Plain Passwords)
Store only a hash of the password. When a user logs in, hash the provided password and compare it to the stored hash. Use a slow hashing algorithm designed for passwords (bcrypt, argon2). In Python, a common approach is passlib.
# requirements (example): passlib[bcrypt] python-jose[cryptography]from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)Token Signing: Secret, Algorithm, Lifetime
- Secret management: keep your signing secret out of source control. Load it from environment variables or a secret manager.
- Algorithm choice: for beginner setups,
HS256(HMAC + shared secret) is common. For larger systems, consider asymmetric algorithms likeRS256(private/public keys). - Token lifetime: short-lived access tokens reduce risk if stolen. A typical starting point is 15–60 minutes.
Step-by-Step: Implement OAuth2 Password Flow + JWT
This section shows a minimal, complete authentication flow. It assumes you already have a User table/model and a way to fetch users from the database (covered elsewhere). To keep the focus on auth, the “get user” function is shown as a placeholder you should connect to your DB layer.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
1) Configuration and JWT Utilities
Create a small module (for example auth.py) to hold auth helpers.
from datetime import datetime, timedelta, timezone
from typing import Optional
from jose import JWTError, jwt
# Load these from environment variables in real apps
SECRET_KEY = "change-me-in-env" # e.g. os.environ["SECRET_KEY"]
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(*, subject: str, expires_delta: Optional[timedelta] = None) -> str:
"""Create a signed JWT. 'subject' is typically a user id or username."""
if expires_delta is None:
expires_delta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.now(timezone.utc) + expires_delta
to_encode = {"sub": subject, "exp": expire}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwtWhy sub? In JWT conventions, sub (subject) identifies the principal (user). Keep it stable (user id or unique username).
2) Define the Token Response Model
FastAPI’s OAuth2 tooling expects a token endpoint that returns access_token and token_type.
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str3) Add OAuth2 Helpers (Password Flow)
FastAPI provides OAuth2PasswordBearer to extract the bearer token from the Authorization header. You also use OAuth2PasswordRequestForm to parse the login form fields (username, password) from the token request.
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")Note: tokenUrl is used by Swagger UI to know where to request tokens. It must match your actual route path.
4) Authenticate the User (Verify Password)
Implement a function that checks credentials. You’ll connect get_user_by_username to your database layer.
from typing import Optional
# Replace with your DB lookup
def get_user_by_username(username: str):
"""Return a user object with at least: username, hashed_password, is_active, role."""
return None
def authenticate_user(username: str, password: str):
user = get_user_by_username(username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return userSecurity note: Avoid revealing whether the username or password was wrong. Return a generic “incorrect username or password”.
5) Create the Token Endpoint
This endpoint receives username/password, validates them, and returns a JWT.
from fastapi import APIRouter
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/token", response_model=Token)
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(subject=user.username)
return {"access_token": access_token, "token_type": "bearer"}Why set WWW-Authenticate? It’s part of the HTTP standard for 401 responses and helps clients understand the required auth scheme.
Protecting Routes with Dependencies
Extract the Current User from the Authorization Header
Now you need a dependency that: (1) reads the token from the header, (2) verifies it, (3) loads the user, and (4) returns the user object to your endpoint.
def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = 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])
username: str | None = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user_by_username(username)
if user is None:
raise credentials_exception
return userReturn 401 vs 403 Correctly
- 401 Unauthorized: the request is missing valid authentication (no token, invalid token, expired token).
- 403 Forbidden: the user is authenticated, but not allowed to access the resource (wrong role, inactive account, missing permission).
Example: block inactive users with 403.
def get_current_active_user(current_user = Depends(get_current_user)):
if not getattr(current_user, "is_active", True):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
)
return current_userRole/Permission Checks (Simple Pattern)
For a beginner-friendly approach, you can implement a small dependency factory that checks a required role.
def require_role(required_role: str):
def _checker(current_user = Depends(get_current_active_user)):
if getattr(current_user, "role", None) != required_role:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return current_user
return _checkerSecure Selected Endpoints
Apply the dependencies to endpoints you want to protect. You can require any authenticated user, only active users, or specific roles.
from fastapi import APIRouter, Depends
api_router = APIRouter()
@api_router.get("/me")
def read_me(current_user = Depends(get_current_active_user)):
return {
"username": current_user.username,
"role": getattr(current_user, "role", None),
}
@api_router.get("/admin/stats")
def read_admin_stats(current_user = Depends(require_role("admin"))):
return {"status": "ok", "requested_by": current_user.username}Tip: You can also secure an entire router by passing dependencies=[Depends(...)] to APIRouter if many endpoints share the same auth requirement.
Token Expiration and Common Failure Modes
Expiration Handling
If you include exp, the JWT library will reject expired tokens during jwt.decode. That should result in a 401 from your get_current_user dependency.
When testing, if you set a very short lifetime (like 1 minute), you’ll quickly see the behavior: after expiration, requests return 401 and clients must re-authenticate (or use a refresh token flow, which is beyond this chapter).
Common Mistakes to Avoid
| Mistake | Why it’s a problem | Better approach |
|---|---|---|
| Storing plain passwords | Immediate account compromise if DB leaks | Store bcrypt/argon2 hashes only |
Hardcoding SECRET_KEY in code | Secrets leak via git, logs, or shared files | Use environment variables / secret manager |
| Very long-lived access tokens | Stolen tokens remain valid too long | Short lifetimes (15–60 min) + refresh tokens later |
| Putting sensitive data in JWT payload | JWT is not encrypted | Keep payload minimal (sub, exp, maybe roles) |
Testing Authentication via Swagger UI
Once your /auth/token endpoint exists and you use OAuth2PasswordBearer(tokenUrl="/auth/token"), Swagger UI can request a token and attach it automatically.
Step-by-Step in Swagger UI
- Open Swagger UI for your API.
- Click the Authorize button.
- Enter
usernameandpassword(Swagger UI will send them to/auth/token). - After authorization, try calling a protected endpoint like
GET /me.
What to Expect When Things Go Wrong
- If you call a protected endpoint without authorizing: 401 with
WWW-Authenticate: Bearer. - If you authorize but your token is expired or invalid: 401.
- If you are authenticated but lack permissions (e.g., non-admin calling
/admin/stats): 403.