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.
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"}
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:
- Client sends username and password to /token endpoint
- Server verifies credentials and generates JWT token
- Client stores token and sends it with each request
- Server validates token before processing request
- 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
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
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
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
- 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.