Mastering FastAPI Security: A Comprehensive Guide to OAuth2 with Password Hashing and JWT Bearer Tokens

In the age of digital transformation, securing web applications has become paramount. FastAPI is one of the most popular frameworks for building APIs with Python, and it provides several options for securing endpoints and managing authentication. This comprehensive guide will walk you through implementing OAuth2 with password hashing and JWT bearer tokens in FastAPI.

Introduction to OAuth2

OAuth2 is a widely adopted authorization framework that allows applications to obtain limited access to user accounts on an HTTP service. It works through the delegation of user authentication to a third-party service, enabling more secure and efficient access control.

Key Concepts of OAuth2

  • Resource Owner: The user who authorizes an application to access their account.
  • Client: The application requesting access to the user's account.
  • Authorization Server: The server that issues tokens after successfully authenticating the Resource Owner and obtaining authorization.
  • Resource Server: The server that houses the user's data and accepts access tokens for resource access.

Setting Up FastAPI for OAuth2

We'll start by setting up a basic FastAPI application and adding the necessary libraries for implementing OAuth2.

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext
from jose import JWTError, jwt
from pydantic import BaseModel
from datetime import datetime, timedelta
from typing import Optional

The libraries and modules you see here are essential for handling dependencies, HTTP exceptions, password hashing, and JSON Web Tokens (JWT).

Implementing Password Hashing

Storing plain text passwords is never a good practice. Instead, we hash passwords using passlib, a password hashing library for Python.

Creating a Password Hashing Function

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)

Here, verify_password checks if the plain text password matches the hashed password, while get_password_hash hashes a plain text password.

Creating Models and Token Utilities

We need user models and utilities for generating and decoding JWT tokens.

User and Token Models

class User(BaseModel):
    username: str

class UserInDB(User):
    hashed_password: str

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

class TokenData(BaseModel):
    username: Optional[str] = None

User represents a user, UserInDB includes the hashed password, and Token and TokenData deal with JWT tokens.

JWT Utilities

SECRET_KEY = "secret"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

The create_access_token function generates JWT tokens with an expiration time.

Handling Authentication and Authorization

We'll use the OAuth2PasswordBearer and OAuth2PasswordRequestForm classes to handle authentication.

Dependency Injection for Secure Endpoints

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async 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
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user

This code validates the JWT token and retrieves the current user from the token data.

Creating Routes for Token Generation and Secure Access

app = FastAPI()

@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, 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_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user

The /token endpoint generates a JWT token, and /users/me returns the current authenticated user's data.

Conclusion

Implementing OAuth2 with password hashing and JWT bearer tokens in FastAPI provides robust security for your APIs. This guide covered the essential components, including setting up FastAPI, implementing password hashing, creating JWT tokens, and securing endpoints. By following these steps, you can enhance the security of your applications while leveraging FastAPI's efficiency.

Ready to take your FastAPI application to the next level? Start implementing these security practices today to safeguard your users and data.