FastAPI Security and Authentication - Interview Questions
1. What are the main security features in FastAPI?
FastAPI provides several built-in security features:
- Automatic Data Validation: Prevents injection attacks through Pydantic validation
- Type Safety: Reduces security vulnerabilities through type checking
- OpenAPI Security Schemes: Built-in support for OAuth2, HTTP Basic, API Keys
- CORS Support: Built-in CORS middleware for cross-origin requests
- HTTPS Support: Easy integration with SSL/TLS
- Dependency Injection: Secure handling of authentication and authorization
- Request/Response Validation: Automatic validation of all inputs and outputs
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
2. How do you implement JWT authentication in FastAPI?
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional
app = FastAPI()
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Configuration
SECRET_KEY = "your-secret-key-here"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Password hashing
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)
# Token creation
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
# Token verification
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user(username)
if user is None:
raise credentials_exception
return user
@app.post("/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(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")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
3. How do you implement OAuth2 with Password flow in FastAPI?
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# User model
class User:
def __init__(self, username: str, hashed_password: str):
self.username = username
self.hashed_password = hashed_password
# Simulated user database
fake_users_db = {
"johndoe": {
"username": "johndoe",
"hashed_password": pwd_context.hash("secret"),
"email": "johndoe@example.com",
"full_name": "John Doe",
"disabled": False,
}
}
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return User(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: 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
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 JWTError:
raise credentials_exception
user = get_user(fake_users_db, username=username)
if user is None:
raise credentials_exception
return user
@app.post("/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")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
4. How do you implement role-based access control (RBAC) in FastAPI?
from fastapi import FastAPI, Depends, HTTPException, status
from enum import Enum
from typing import List
app = FastAPI()
class Role(str, Enum):
ADMIN = "admin"
USER = "user"
MODERATOR = "moderator"
class User:
def __init__(self, username: str, roles: List[Role]):
self.username = username
self.roles = roles
def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
# Token validation logic here
return User(username="john", roles=[Role.USER])
def require_role(required_role: Role):
def role_checker(current_user: User = Depends(get_current_user)):
if required_role not in current_user.roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user
return role_checker
def require_any_role(required_roles: List[Role]):
def role_checker(current_user: User = Depends(get_current_user)):
if not any(role in current_user.roles for role in required_roles):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user
return role_checker
# Endpoints with role-based access
@app.get("/admin-only")
async def admin_only(current_user: User = Depends(require_role(Role.ADMIN))):
return {"message": "Admin access granted", "user": current_user.username}
@app.get("/moderator-or-admin")
async def moderator_or_admin(
current_user: User = Depends(require_any_role([Role.MODERATOR, Role.ADMIN]))
):
return {"message": "Moderator or admin access granted", "user": current_user.username}
@app.get("/public")
async def public_endpoint():
return {"message": "Public access"}
@app.get("/user-only")
async def user_only(current_user: User = Depends(require_role(Role.USER))):
return {"message": "User access granted", "user": current_user.username}
5. How do you implement API key authentication in FastAPI?
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
from typing import Optional
app = FastAPI()
# API Key in header
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
# API Key in query parameter
api_key_query = APIKeyQuery(name="api_key", auto_error=False)
# Simulated API key database
API_KEYS = {
"test-api-key-1": {"user_id": 1, "permissions": ["read", "write"]},
"test-api-key-2": {"user_id": 2, "permissions": ["read"]},
}
def get_api_key_user(api_key: str = Depends(api_key_header)):
if api_key not in API_KEYS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API key"
)
return API_KEYS[api_key]
def require_permission(permission: str):
def permission_checker(api_key_user: dict = Depends(get_api_key_user)):
if permission not in api_key_user["permissions"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permission '{permission}' required"
)
return api_key_user
return permission_checker
@app.get("/protected")
async def protected_endpoint(api_key_user: dict = Depends(get_api_key_user)):
return {"message": "Access granted", "user_id": api_key_user["user_id"]}
@app.get("/read-only")
async def read_only_endpoint(api_key_user: dict = Depends(require_permission("read"))):
return {"message": "Read access granted", "user_id": api_key_user["user_id"]}
@app.post("/write-endpoint")
async def write_endpoint(api_key_user: dict = Depends(require_permission("write"))):
return {"message": "Write access granted", "user_id": api_key_user["user_id"]}
# Alternative: API key in query parameter
@app.get("/query-key")
async def query_key_endpoint(api_key: str = Depends(api_key_query)):
if api_key not in API_KEYS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API key"
)
return {"message": "Access granted via query parameter"}
6. How do you implement CORS (Cross-Origin Resource Sharing) in FastAPI?
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Basic CORS setup
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "https://yourdomain.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
# More restrictive CORS setup
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"https://yourdomain.com",
"https://api.yourdomain.com"
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=[
"Accept",
"Accept-Language",
"Content-Language",
"Content-Type",
"Authorization",
"X-Requested-With"
],
expose_headers=["Content-Length", "X-Total-Count"],
max_age=600, # Cache preflight requests for 10 minutes
)
# Environment-based CORS
import os
origins = [
"http://localhost:3000",
"http://localhost:8080",
]
if os.getenv("ENVIRONMENT") == "production":
origins = [
"https://yourdomain.com",
"https://www.yourdomain.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
7. How do you implement rate limiting for security in FastAPI?
from fastapi import FastAPI, HTTPException, Depends, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from collections import defaultdict
import time
app = FastAPI()
# Using slowapi for rate limiting
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/api/data")
@limiter.limit("5/minute")
async def get_data(request: Request):
return {"data": "some data"}
@app.post("/api/login")
@limiter.limit("3/minute")
async def login(request: Request):
return {"message": "Login attempt"}
# Custom rate limiting implementation
class SecurityRateLimiter:
def __init__(self):
self.requests = defaultdict(list)
self.limits = {
"login": {"requests": 5, "window": 300}, # 5 requests per 5 minutes
"api": {"requests": 100, "window": 60}, # 100 requests per minute
"upload": {"requests": 10, "window": 3600} # 10 uploads per hour
}
def is_allowed(self, client_id: str, endpoint: str) -> bool:
now = time.time()
limit_config = self.limits.get(endpoint, {"requests": 60, "window": 60})
# Clean old requests
window_start = now - limit_config["window"]
self.requests[f"{client_id}:{endpoint}"] = [
req_time for req_time in self.requests[f"{client_id}:{endpoint}"]
if req_time > window_start
]
current_requests = len(self.requests[f"{client_id}:{endpoint}"])
if current_requests >= limit_config["requests"]:
return False
self.requests[f"{client_id}:{endpoint}"].append(now)
return True
security_limiter = SecurityRateLimiter()
def check_rate_limit(endpoint: str):
def rate_limit_checker(request: Request):
client_id = request.client.host
if not security_limiter.is_allowed(client_id, endpoint):
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded for {endpoint}"
)
return rate_limit_checker
@app.post("/login")
async def login_endpoint(request: Request):
check_rate_limit("login")(request)
return {"message": "Login successful"}
@app.get("/api/protected")
async def protected_api(request: Request):
check_rate_limit("api")(request)
return {"data": "protected data"}
8. How do you implement input validation and sanitization for security?
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, validator, Field
import re
from typing import Optional
import html
app = FastAPI()
class UserInput(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: str = Field(..., regex=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
password: str = Field(..., min_length=8)
bio: Optional[str] = Field(None, max_length=500)
@validator('username')
def validate_username(cls, v):
# Check for SQL injection patterns
sql_patterns = [
r"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b)",
r"(\b(UNION|EXEC|EXECUTE)\b)",
r"(--|/\*|\*/|xp_|sp_)",
]
for pattern in sql_patterns:
if re.search(pattern, v, re.IGNORECASE):
raise ValueError("Invalid username format")
# Check for XSS patterns
xss_patterns = [
r"<script.*?>.*?</script>",
r"javascript:",
r"on\w+\s*=",
]
for pattern in xss_patterns:
if re.search(pattern, v, re.IGNORECASE):
raise ValueError("Invalid username format")
return v.strip()
@validator('bio')
def sanitize_bio(cls, v):
if v:
# HTML escape to prevent XSS
v = html.escape(v)
# Remove potentially dangerous tags
v = re.sub(r'<[^>]*>', '', v)
return v
return v
class CommentInput(BaseModel):
content: str = Field(..., max_length=1000)
@validator('content')
def sanitize_content(cls, v):
# Remove HTML tags
v = re.sub(r'<[^>]*>', '', v)
# HTML escape
v = html.escape(v)
# Remove excessive whitespace
v = re.sub(r'\s+', ' ', v).strip()
return v
@app.post("/users")
async def create_user(user: UserInput):
# Additional server-side validation
if user.username.lower() in ['admin', 'root', 'system']:
raise HTTPException(status_code=400, detail="Username not allowed")
return {"message": "User created", "username": user.username}
@app.post("/comments")
async def create_comment(comment: CommentInput):
return {"message": "Comment created", "content": comment.content}
# Custom validation decorator
def validate_file_upload(file_size_limit: int = 5 * 1024 * 1024): # 5MB
def validator(request: Request):
content_length = request.headers.get('content-length')
if content_length and int(content_length) > file_size_limit:
raise HTTPException(
status_code=413,
detail=f"File too large. Maximum size is {file_size_limit} bytes"
)
return validator
@app.post("/upload")
async def upload_file(request: Request):
validate_file_upload()(request)
return {"message": "File upload validated"}
9. How do you implement secure headers in FastAPI?
from fastapi import FastAPI, Request, Response
from fastapi.middleware.trustedhost import TrustedHostMiddleware
import secrets
app = FastAPI()
# Trusted Host middleware
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["yourdomain.com", "*.yourdomain.com"]
)
# Custom middleware for security headers
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
# Security headers
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"
response.headers["Content-Security-Policy"] = "default-src 'self'"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "geolocation=(), microphone=()"
# Remove server information
response.headers.pop("Server", None)
return response
# CSRF protection
class CSRFMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
# Add CSRF token to state
scope["state"]["csrf_token"] = secrets.token_urlsafe(32)
await self.app(scope, receive, send)
app = CSRFMiddleware(app)
# Session security
from fastapi import FastAPI, Request, Response
from starlette.middleware.sessions import SessionMiddleware
app.add_middleware(
SessionMiddleware,
secret_key="your-secret-key-here",
max_age=3600, # 1 hour
same_site="lax",
https_only=True # In production
)
@app.post("/secure-action")
async def secure_action(request: Request):
# Verify CSRF token
csrf_token = request.state.get("csrf_token")
form_token = request.form().get("csrf_token")
if not csrf_token or csrf_token != form_token:
raise HTTPException(status_code=403, detail="CSRF token invalid")
return {"message": "Action completed securely"}
10. How do you implement secure file uploads in FastAPI?
from fastapi import FastAPI, File, UploadFile, HTTPException, Depends
from fastapi.responses import FileResponse
import os
import hashlib
import magic
from typing import List
import aiofiles
app = FastAPI()
# Allowed file types
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt"}
ALLOWED_MIME_TYPES = {
"image/jpeg", "image/png", "image/gif",
"application/pdf", "text/plain"
}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
def validate_file(file: UploadFile):
# Check file size
if file.size and file.size > MAX_FILE_SIZE:
raise HTTPException(
status_code=413,
detail=f"File too large. Maximum size is {MAX_FILE_SIZE} bytes"
)
# Check file extension
file_extension = os.path.splitext(file.filename)[1].lower()
if file_extension not in ALLOWED_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"File type not allowed. Allowed types: {ALLOWED_EXTENSIONS}"
)
return file
async def validate_file_content(file: UploadFile):
# Read first 2048 bytes to check MIME type
content = await file.read(2048)
await file.seek(0) # Reset file pointer
# Check MIME type using python-magic
mime_type = magic.from_buffer(content, mime=True)
if mime_type not in ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=400,
detail=f"File content type not allowed: {mime_type}"
)
return file
def secure_filename(filename: str) -> str:
"""Generate a secure filename"""
# Remove path separators and dangerous characters
filename = os.path.basename(filename)
filename = re.sub(r'[^\w\-_\.]', '_', filename)
# Add hash to prevent conflicts
name, ext = os.path.splitext(filename)
hash_suffix = hashlib.md5(name.encode()).hexdigest()[:8]
return f"{name}_{hash_suffix}{ext}"
@app.post("/upload")
async def upload_file(
file: UploadFile = Depends(validate_file),
validated_file: UploadFile = Depends(validate_file_content)
):
# Generate secure filename
secure_name = secure_filename(file.filename)
file_path = f"uploads/{secure_name}"
# Ensure upload directory exists
os.makedirs("uploads", exist_ok=True)
# Save file securely
async with aiofiles.open(file_path, 'wb') as f:
content = await file.read()
await f.write(content)
return {
"filename": secure_name,
"size": len(content),
"message": "File uploaded successfully"
}
@app.get("/files/{filename}")
async def get_file(filename: str):
file_path = f"uploads/{filename}"
# Validate filename to prevent path traversal
if not os.path.exists(file_path) or ".." in filename:
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(file_path)
# Multiple file upload with validation
@app.post("/upload-multiple")
async def upload_multiple_files(
files: List[UploadFile] = File(...)
):
uploaded_files = []
for file in files:
try:
validated_file = validate_file(file)
validated_content = await validate_file_content(validated_file)
secure_name = secure_filename(file.filename)
file_path = f"uploads/{secure_name}"
async with aiofiles.open(file_path, 'wb') as f:
content = await file.read()
await f.write(content)
uploaded_files.append({
"filename": secure_name,
"size": len(content)
})
except HTTPException as e:
return {"error": f"Error uploading {file.filename}: {e.detail}"}
return {"uploaded_files": uploaded_files}
Interview angle
- “How do you implement auth in FastAPI?” - a dependency that extracts and validates the credential and returns the current user, applied per route or per router.
OAuth2PasswordBearerwires the token extraction and the OpenAPI security scheme together. - “Where do you store the token in a browser client?” - an httpOnly, Secure, SameSite cookie rather than localStorage, which is readable by any XSS. If you use cookies you must handle CSRF; with a bearer header you don’t, but you’ve taken on XSS exposure instead.
- “How do you revoke a JWT?” - you can’t directly, which is the trade. Use short-lived access tokens with refresh tokens, plus a denylist keyed on a token id for immediate revocation. See ../../11_authentication/jwt/05_revocation_logout.md.
- “What’s easy to get wrong?” - verifying the signature but not the algorithm, audience or expiry; accepting
alg: none; and putting authorisation decisions in the token rather than checking them server-side at use time.