DEV Community

Borino88
Borino88

Posted on

Building a Secure API Starter Without Hiding the Hard Parts

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) ]
Enter fullscreen mode Exit fullscreen mode

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")}
Enter fullscreen mode Exit fullscreen mode

3. Threat Modeling & Secure Storage

Token Storage Recommendations

  1. Never store tokens in localStorage: They are vulnerable to Cross-Site Scripting (XSS) attacks.
  2. Use HttpOnly, Secure, SameSite=Strict Cookies: This ensures the browser only sends tokens during first-party requests and hides them from client-side scripts.
  3. Mobile Client Storage: Exclusively utilize hardware-backed security modules (Keychain for iOS, Keystore for Android).

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

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?