Free Ebook cover FastAPI for Beginners: Build a Production-Ready REST API

FastAPI for Beginners: Build a Production-Ready REST API

New course

14 pages

Authentication Basics with OAuth2 and JWT in FastAPI

Capítulo 11

Estimated reading time: 8 minutes

+ Exercise

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 like RS256 (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 App

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_jwt

Why 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: str

3) 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 user

Security 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 user

Return 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_user

Role/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 _checker

Secure 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

MistakeWhy it’s a problemBetter approach
Storing plain passwordsImmediate account compromise if DB leaksStore bcrypt/argon2 hashes only
Hardcoding SECRET_KEY in codeSecrets leak via git, logs, or shared filesUse environment variables / secret manager
Very long-lived access tokensStolen tokens remain valid too longShort lifetimes (15–60 min) + refresh tokens later
Putting sensitive data in JWT payloadJWT is not encryptedKeep 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 username and password (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.

Now answer the exercise about the content:

In a FastAPI app using OAuth2 Password flow with JWT, which situation should result in a 403 Forbidden response instead of 401 Unauthorized?

You are right! Congratulations, now go to the next page

You missed! Try again.

Use 401 when authentication is missing or invalid (no token, bad token, expired token). Use 403 when the user is authenticated but not allowed (inactive account or insufficient permissions).

Next chapter

Configuration Management and Environment Separation

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.