Building a Secure API Starter Without Hiding the Hard Parts
Exposing REST APIs to the public internet requires defense-in-depth security. Too many tutorial implementations cut corners by skipping rate limiting, using simple database lookups for every request, or storing JSON Web Tokens (JWTs) in browser LocalStorage.
Here is how to design a secure FastAPI starter that addresses authentication, rate limiting, and secure storage from day one.
1. Defense-in-Depth Architecture
To build a secure API, you need multiple layers of protection:
[ Client ]
│
▼
[ Rate Limiting Layer (Token Bucket / Redis) ]
│
▼
[ CORS Policy Guards ]
│
▼
[ Stateless JWT / OAuth2 Security Middleware ]
│
▼
[ Controller / Role-Based Access Control (RBAC) ]
2. Stateless Auth & JWT Revocation
While stateless JWT authentication is clean, token revocation (for logouts or password resets) is often neglected. We solve this by adding a Redis-backed token blacklist. We store the token's unique identifier (jti) in Redis with a Time-To-Live (TTL) matching the token's remaining lifespan.
Here is the FastAPI implementation:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from datetime import datetime, timedelta
SECRET_KEY = "your-high-entropy-source-key"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Mock Redis blacklist for demonstration
token_blacklist = set()
def revoke_token(jti: str):
token_blacklist.add(jti)
def is_token_revoked(jti: str) -> bool:
return jti in token_blacklist
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")
jti: str = payload.get("jti")
if username is None or jti is None:
raise credentials_exception
if is_token_revoked(jti):
raise HTTPException(status_code=401, detail="Token has been revoked")
except JWTError:
raise credentials_exception
return {"username": username, "role": payload.get("role", "user")}
3. Threat Modeling & Secure Storage
Token Storage Recommendations
-
Never store tokens in
localStorage: They are vulnerable to Cross-Site Scripting (XSS) attacks. -
Use
HttpOnly,Secure,SameSite=StrictCookies: This ensures the browser only sends tokens during first-party requests and hides them from client-side scripts. - Mobile Client Storage: Exclusively utilize hardware-backed security modules (Keychain for iOS, Keystore for Android).
Top comments (1)
I appreciate how you've emphasized the importance of defense-in-depth security, particularly with the layered approach outlined in your architecture diagram. The addition of a Redis-backed token blacklist for stateless JWT authentication is a great way to handle token revocation, and I've found similar implementations to be effective in preventing unauthorized access. One potential consideration for further enhancing security could be integrating additional factors, such as IP blocking or behavioral analysis, to supplement the rate limiting and token validation. Have you explored any of these advanced security measures in your own implementations or seen them in production environments?