FastAPI

FastAPI Authentication & Authorization: JWT, OAuth2, and RBAC

Secure Your APIs with JWT, OAuth2, and Role-Based Access Control

Introduction to FastAPI Security

For secure production APIs, FastAPI’s built-in security features provide a comprehensive foundation for implementing authentication and authorization. FastAPI offers native support for OAuth2, JWT tokens, and dependency injection patterns that make securing your APIs straightforward and production-ready.

Why FastAPI Security Matters: Security isn’t optional—it’s fundamental to production applications. FastAPI makes implementing industry-standard security patterns simple without sacrificing performance or flexibility.

Security Fundamentals

Before diving into implementation, understand the distinction between authentication and authorization:

Authentication

  • Verifies user identity
  • “Who are you?”
  • Username/password validation
  • Token generation and verification
  • Session management

Authorization

  • Grants permissions
  • “What can you do?”
  • Role-based access (RBAC)
  • Permission checking
  • Resource ownership

A user might be authenticated (we know who they are) but not authorized (they can’t access this resource). FastAPI provides tools for both through its dependency injection system and security utilities.

JWT (JSON Web Tokens) Explained

What is JWT?

JWT (JSON Web Tokens) is a stateless authentication mechanism that encodes user information in a cryptographically signed token. Unlike session-based authentication that requires server-side storage, JWTs are self-contained and can be validated by any server that has the secret key.

A JWT consists of three parts separated by dots:

  • Header: Token type (JWT) and algorithm (HS256, RS256)
  • Payload: Claims (user ID, email, roles, permissions)
  • Signature: HMAC or RSA signature for verification

Format: header.payload.signature

JWT Implementation in FastAPI

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

# Configuration
SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

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

class User(BaseModel):
    username: str
    email: str
    disabled: bool = False

class UserInDB(User):
    hashed_password: str

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)

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

app = FastAPI()

@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    # Verify user credentials (implementation depends on your database)
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid credentials"
        )
    
    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"}
Security Alert: Never commit SECRET_KEY to version control. Store it in environment variables using tools like python-dotenv or managed secrets services.
See also  FastAPI Complete Guide: Building Production APIs

OAuth2 Authentication Flow

OAuth2 with Password Flow

OAuth2 is an authorization framework that defines how clients can obtain tokens from an authorization server. FastAPI simplifies OAuth2 implementation through the OAuth2PasswordBearer class.

The password flow (suitable for trusted first-party applications) works as follows:

  1. Client sends username and password to /token endpoint
  2. Server verifies credentials and generates JWT token
  3. Client stores token and sends it with each request
  4. Server validates token before processing request
  5. If token expires, client requests new token

Token Validation and Current User Dependency

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
    except jwt.InvalidTokenError:
        raise credentials_exception
    
    user = get_user_from_db(username)
    if user is None:
        raise credentials_exception
    return user

@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user
Pro Tip: Use dependency injection to keep your code DRY. The Depends() function allows reusing authentication logic across multiple endpoints.

Role-Based Access Control (RBAC)

Implementing Role-Based Authorization

RBAC assigns permissions based on user roles (admin, moderator, user, guest). FastAPI’s dependency system makes RBAC implementation elegant and reusable.

Role-Based Dependencies

from typing import List

def require_role(*allowed_roles: str):
    async def role_checker(current_user: User = Depends(get_current_user)):
        if current_user.role not in allowed_roles:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Insufficient permissions"
            )
        return current_user
    return role_checker

# Usage
@app.get("/admin-panel")
async def admin_panel(current_user: User = Depends(require_role("admin"))):
    return {"message": f"Welcome Admin {current_user.username}"}

@app.get("/users")
async def list_users(
    current_user: User = Depends(require_role("admin", "moderator"))
):
    return {"users": []}  # Return user list

Permission-Based Access Control

def require_permission(permission: str):
    async def permission_checker(current_user: User = Depends(get_current_user)):
        if permission not in current_user.permissions:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Missing permission: {permission}"
            )
        return current_user
    return permission_checker

# Usage
@app.post("/items")
async def create_item(
    item: Item,
    current_user: User = Depends(require_permission("create:items"))
):
    return {"created_item": item}

RBAC Best Practices

  • Define roles clearly (admin, user, guest, etc.)
  • Assign minimum necessary permissions
  • Use dependencies for reusable permission checks
  • Log access attempts and permission denials
  • Review and audit role assignments regularly
See also  FastAPI Complete Guide: Building Production APIs

Production Implementation Example

Complete Authentication Flow

from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel

app = FastAPI()

class UserRegister(BaseModel):
    username: str
    email: str
    password: str

class UserInDBResponse(BaseModel):
    id: int
    username: str
    email: str
    role: str

@app.post("/register", response_model=Token)
async def register_user(user: UserRegister, db: Session = Depends(get_db)):
    # Check if user exists
    existing_user = db.query(DBUser).filter(
        DBUser.username == user.username
    ).first()
    
    if existing_user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    # Hash password and create user
    hashed_password = get_password_hash(user.password)
    db_user = DBUser(
        username=user.username,
        email=user.email,
        hashed_password=hashed_password,
        role="user"  # Default role
    )
    
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    
    # Generate token
    access_token = create_access_token(data={"sub": db_user.username})
    return {"access_token": access_token, "token_type": "bearer"}

@app.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
    db_user = db.query(DBUser).filter(DBUser.username == form_data.username).first()
    
    if not db_user or not verify_password(form_data.password, db_user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid credentials"
        )
    
    access_token = create_access_token(data={"sub": db_user.username})
    return {"access_token": access_token, "token_type": "bearer"}

Security Best Practices

Critical Security Guidelines

  • Use HTTPS in Production: Always use SSL/TLS to encrypt JWT tokens in transit
  • Secure Secret Key: Generate random 32+ character keys, store in environment variables
  • Token Expiration: Set reasonable expiration times (15-30 minutes for access tokens)
  • Refresh Tokens: Use separate long-lived refresh tokens for obtaining new access tokens
  • CORS Configuration: Restrict cross-origin requests to trusted domains only
  • Rate Limiting: Implement rate limiting on authentication endpoints to prevent brute force
  • Password Hashing: Always use bcrypt or similar with sufficient rounds (12+)
  • Audit Logging: Log authentication failures and permission denials
See also  FastAPI Complete Guide: Building Production APIs

CORS and Security Headers

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourdomain.com"],  # Production domains only
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Add security headers
@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    return response

Rate Limiting on Auth Endpoints

from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

@app.post("/token")
@limiter.limit("5/minute")  # Max 5 login attempts per minute
async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
    # Login logic
    pass
Never:

  • Store passwords in plain text
  • Expose SECRET_KEY in logs or error messages
  • Use weak or reused secret keys
  • Hardcode credentials or API keys
  • Send sensitive data in JWT without HTTPS

The Bottom Line

Implementing proper authentication and authorization in FastAPI is straightforward with its built-in security features. By combining JWT tokens, OAuth2, and role-based access control, you can build APIs that are both secure and user-friendly.

The key is to follow best practices: use strong cryptography, implement proper token expiration, validate on every request, and audit access patterns. FastAPI’s dependency injection system makes applying these security patterns consistently across your entire application simple and maintainable.

Start with basic username/password authentication using JWT, then layer on RBAC as your application grows. Monitor security advisories for your dependencies and keep your secret keys safe. With these practices in place, your FastAPI applications will be secure, scalable, and production-ready.