Python Fastapi Jwt Oauth2 Authentication

Written by

in

When you’re building modern APIs with Python, security is not an after‑thought—it’s the backbone that protects your data and users. FastAPI has quickly become the go‑to framework for high‑performance, asynchronous web services, and pairing it with JWT (JSON Web Tokens) and OAuth2 gives you a robust, scalable authentication system that works across browsers, mobile apps, and micro‑services. In this guide we’ll walk through the concepts, best practices, and step‑by‑step code you need to implement JWT‑based OAuth2 authentication in a FastAPI project, while keeping SEO keywords like “FastAPI JWT OAuth2 authentication” front and center for search engines.

Why Choose FastAPI for Authentication?

FastAPI shines for authentication for three main reasons:

  • Speed: Built on Starlette and Pydantic, FastAPI delivers sub‑millisecond response times, which is crucial when you’re validating tokens on every request.
  • Declarative security: Dependency injection lets you declare security requirements directly in your path operations, making the code clean and maintainable.
  • Automatic docs: OpenAPI (Swagger) integration automatically documents your OAuth2 flows, helping developers understand how to obtain and use tokens.

Understanding JWT and OAuth2 Basics

What Is a JWT?

A JWT is a compact, URL‑safe token that consists of three Base64‑encoded parts: header, payload, and signature. The payload typically contains user claims (e.g., sub, exp, roles) and is signed with a secret key or an asymmetric key pair, ensuring integrity.

OAuth2 Flow in a Nutshell

OAuth2 is an authorization framework that defines how clients obtain access tokens from an authorization server. The most common flow for APIs is the Resource Owner Password Credentials or Authorization Code with PKCE for public clients. FastAPI’s OAuth2PasswordBearer utility simplifies the “password” flow, while OAuth2AuthorizationCodeBearer (available via extensions) handles the code flow.

Project Setup: Installing the Essentials

Before writing any code, set up a clean virtual environment and install the required packages:

python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install fastapi uvicorn python-multipart[all] python-jose[cryptography] passlib[bcrypt] sqlalchemy

These packages give you FastAPI, an ASGI server (Uvicorn), JWT handling (python-jose), password hashing (passlib), and a lightweight ORM (SQLAlchemy) for user storage.

Step‑by‑Step Implementation

1. Define the User Model

For demonstration we’ll use an in‑memory SQLite database with SQLAlchemy. In a real‑world app you’d replace this with PostgreSQL or another production‑grade DB.

from sqlalchemy import Column, Integer, String, Boolean, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    is_active = Column(Boolean, default=True)

2. Create Utility Functions for Passwords and Tokens

We’ll use passlib for hashing passwords and python-jose for creating/validating JWTs.

from passlib.context import CryptContext
from datetime import datetime, timedelta
from jose import JWTError, jwt

SECRET_KEY = "your-secret-key-should-be-long-and-random"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

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

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

def get_password_hash(password):
    return pwd_context.hash(password)

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})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

3. Dependency to Get the Current User

FastAPI’s Depends makes it easy to inject authentication logic into any endpoint.

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

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_user(db, username: str):
    return db.query(User).filter(User.username == username).first()

def get_current_user(token: str = Depends(oauth2_scheme), db: SessionLocal = Depends()):
    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 JWTError:
        raise credentials_exception
    user = get_user(db, username)
    if user is None:
        raise credentials_exception
    return user

4. Token Endpoint (OAuth2 Password Flow)

This endpoint validates the user’s credentials and returns a signed JWT.

from fastapi import FastAPI, Form
from pydantic import BaseModel

app = FastAPI()

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: str | None = None

@app.post("/token", response_model=Token, tags=["authentication"])
async def login_for_access_token(form_data: dict = Depends()):
    # FastAPI automatically parses form fields: grant_type, username, password
    db = SessionLocal()
    user = get_user(db, form_data["username"])
    if not user or not verify_password(form_data["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})
    return {"access_token": access_token, "token_type": "bearer"}

5. Protecting Routes with JWT Authentication

Now any route that includes Depends(get_current_user) will require a valid JWT.

@app.get("/users/me", tags=["users"])
async def read_users_me(current_user: User = Depends(get_current_user)):
    return {"username": current_user.username, "is_active": current_user.is_active}

6. Adding Role‑Based Access Control (Optional)

If your API needs fine‑grained permissions, store a role claim inside the JWT and create a custom dependency.

def require_role(required_role: str):
    async def role_checker(current_user: User = Depends(get_current_user)):
        if getattr(current_user, "role", None) != required_role:
            raise HTTPException(status_code=403, detail="Insufficient privileges")
        return current_user
    return role_checker

@app.get("/admin/dashboard", tags=["admin"])
async def admin_dashboard(user: User = Depends(require_role("admin"))):
    return {"msg": f"Welcome, {user.username}. You have admin access."}

Best Practices for Secure FastAPI JWT OAuth2 Authentication

  • Use HTTPS everywhere. Tokens travel in headers; without TLS they can be intercepted.
  • Rotate secrets regularly. Store SECRET_KEY in environment variables or secret managers, not in source code.
  • Set short expiration times. A 15‑30 minute access token reduces the window for replay attacks. Pair it with refresh tokens if you need long‑term sessions.
  • Validate exp and nbf claims. The python-jose library does this automatically, but custom checks add an extra safety net.
  • Implement token revocation. Store a blacklist of revoked JWT IDs (jti) in Redis or a DB if you need immediate logout capabilities.
  • Limit scopes. Use the scope claim to restrict what each token can do, and enforce scope checks in your route dependencies.

Testing Your Authentication Flow

FastAPI’s built‑in TestClient (powered by requests) makes it easy to write automated tests.

from fastapi.testclient import TestClient

client = TestClient(app)

def test_login_and_access():
# Create a test user first (omitted for brevity)
response = client.post(
"/token",
data={"username": "alice", "password": "secret"},
)
assert response.status_code == 200
token = response.json()["access_token"]

# Access a protected endpoint
protected = client.get(
"/users/me",
headers={"Authorization": f"Bearer {token}"}
)

Comments

Leave a Reply

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