Python Fastapi Jwt Oauth2 Authentication

Written by

in

When you’re building modern APIs with Python, speed, scalability, and security are non‑negotiable. FastAPI has quickly become the go‑to framework for developers who want lightning‑fast performance without sacrificing developer experience. Yet, an API is only as good as its authentication strategy. In this guide we’ll walk through a complete, production‑ready implementation of JWT (JSON Web Token) based OAuth2 authentication in FastAPI, covering everything from token generation to token revocation, best practices, and common pitfalls. By the end, you’ll have a solid, reusable authentication module that you can drop into any FastAPI project.

Why Choose JWT and OAuth2 with FastAPI?

FastAPI’s native support for OAuth2PasswordBearer and Security utilities makes it a natural fit for JWT‑based flows. Here are the top reasons to pair them:

  • Statelessness: JWTs carry all the user claims, so you don’t need a server‑side session store.
  • Scalability: Stateless tokens work seamlessly across multiple instances or containers.
  • Interoperability: JWT follows an open standard (RFC 7519) that many services understand.
  • FastAPI integration: Built‑in dependencies simplify token validation and error handling.

Core Concepts You Need to Know

1. OAuth2 Grant Types

For most API‑first applications the Resource Owner Password Credentials grant (often called “password flow”) is the simplest to implement. It lets a client exchange a username and password for an access token. In production you might switch to Authorization Code or Client Credentials grants for higher security, but the password flow is perfect for demos and internal tools.

2. JWT Structure

A JWT consists of three Base64URL‑encoded parts separated by dots:

header.payload.signature

The header defines the signing algorithm (e.g., HS256). The payload holds claims like sub (subject), exp (expiration), and custom fields such as role. The signature ensures the token hasn’t been tampered with.

Step‑by‑Step Implementation

1. Project Setup

First, create a virtual environment and install the required packages:

python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install fastapi uvicorn python‑jwt passlib[bcrypt] python‑dotenv

2. Define Settings and Secrets

Store secret keys and token lifetimes in a .env file. Never hard‑code them.

# .env
SECRET_KEY=supersecretkey12345
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30

Load them with python‑dotenv:

from pathlib import Path
from dotenv import load_dotenv
import os

env_path = Path('.') / '.env'
load_dotenv(dotenv_path=env_path)

SECRET_KEY = os.getenv('SECRET_KEY')
ALGORITHM = os.getenv('ALGORITHM')
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES'))

3. User Model & Password Hashing

For simplicity we’ll use an in‑memory “database”. In real projects replace it with SQLAlchemy or another ORM.

from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

fake_users_db = {
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderland",
        "hashed_password": pwd_context.hash("wonderland123"),
        "disabled": False,
        "role": "admin",
    },
    "bob": {
        "username": "bob",
        "full_name": "Bob Builder",
        "hashed_password": pwd_context.hash("canwefixit"),
        "disabled": False,
        "role": "user",
    },
}

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_user(db, username: str):
    return db.get(username)

4. Token Creation Utility

This function builds a JWT with an expiration claim.

from datetime import datetime, timedelta
import jwt  # PyJWT

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

5. OAuth2 Dependency

FastAPI provides OAuth2PasswordBearer which extracts the token from the Authorization: Bearer header.

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

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 = payload.get("sub")
        if username is None:
            raise credentials_exception
    except jwt.PyJWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username)
    if user is None:
        raise credentials_exception
    return user

6. Login Endpoint

The /token route authenticates a user and returns a JWT.

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/token")
async def login_for_access_token(username: str = Form(...), password: str = Form(...)):
    user = get_user(fake_users_db, username)
    if not user or not verify_password(password, user["hashed_password"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token = create_access_token(data={"sub": user["username"], "role": user["role"]})
    return {"access_token": access_token, "token_type": "bearer"}

7. Protecting Routes

Use the Depends injection to enforce authentication on any endpoint.

@app.get("/users/me")
async def read_current_user(current_user: dict = Depends(get_current_user)):
    return {"username": current_user["username"], "role": current_user["role"]}

8. Role‑Based Access Control (RBAC)

Extend the security layer by checking a user’s role before allowing certain actions.

def require_role(required_role: str):
    async def role_checker(current_user: dict = Depends(get_current_user)):
        if current_user["role"] != required_role:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Insufficient privileges",
            )
        return current_user
    return role_checker

@app.delete("/admin/users/{username}")
async def delete_user(username: str, _: dict = Depends(require_role("admin"))):
    # Placeholder delete logic
    return {"msg": f"User {username} deleted by admin."}

Best Practices for Production‑Ready JWT Auth

  • Use Asymmetric Keys – Switch from HS256 to RS256 (public/private key pair) to avoid sharing the secret across services.
  • Short‑Lived Access Tokens + Refresh Tokens – Issue access tokens that expire in minutes and a separate long‑lived refresh token stored securely (e.g., HttpOnly cookie).
  • Token Revocation – Keep a blacklist or use a token version field in the user record to invalidate tokens after password change or logout.
  • Secure Transport – Always serve APIs over HTTPS; never expose JWTs in URLs.
  • Validate Claims Thoroughly – Check exp, nbf, iss, and custom claims relevant to your domain.
  • Logging & Monitoring – Record authentication attempts and monitor for abnormal token usage patterns.

Testing Your Authentication Flow

FastAPI ships with TestClient (based on requests) which makes it easy to write integration tests.

from fastapi.testclient import TestClient

client = TestClient(app)

def test_login_and_access_protected_route():
    # 1️⃣ Login
    response = client.post(
        "/token",
        data={"username": "alice", "password": "wonderland123"},
    )
    assert response.status_code == 200
    token = response.json()["access_token"]

    # 2️⃣ Access protected endpoint
    protected = client.get(
        "/users/me",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert protected.status_code == 200
    assert protected.json()["username"] == "alice"

Deploying FastAPI with JWT Auth

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *