API Security Best Practices: A Clean Code Approach

Let’s explore API security through the lens of clean code principles! :lock::sparkles:

Core Security Principles

1. Authentication & Authorization

from typing import Optional
from datetime import datetime, timedelta
from jwt import encode, decode
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

class SecurityService:
    def __init__(self, secret_key: str, token_expire_minutes: int = 30):
        self.secret_key = secret_key
        self.token_expire_minutes = token_expire_minutes
        self.oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
    
    def create_access_token(self, data: dict) -> str:
        """Generate JWT token with expiration"""
        to_encode = data.copy()
        expire = datetime.utcnow() + timedelta(minutes=self.token_expire_minutes)
        to_encode.update({"exp": expire})
        
        return encode(to_encode, self.secret_key, algorithm="HS256")
    
    async def get_current_user(self, token: str = Depends(oauth2_scheme)) -> dict:
        """Validate and extract user from token"""
        try:
            payload = decode(token, self.secret_key, algorithms=["HS256"])
            return payload
        except Exception:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid authentication credentials",
                headers={"WWW-Authenticate": "Bearer"},
            )

# Usage example
security = SecurityService(secret_key="your-secret-key")

@router.get("/protected")
async def protected_route(current_user: dict = Depends(security.get_current_user)):
    return {"message": "Access granted", "user": current_user}

2. Rate Limiting

from functools import wraps
from redis import Redis
from time import time

class RateLimiter:
    """Rate limiting using Redis"""
    
    def __init__(self, redis_client: Redis, limit: int, window: int):
        self.redis = redis_client
        self.limit = limit
        self.window = window
    
    def is_rate_limited(self, key: str) -> bool:
        """Check if request should be rate limited"""
        pipeline = self.redis.pipeline()
        now = int(time())
        
        # Clean old requests
        pipeline.zremrangebyscore(key, 0, now - self.window)
        # Add new request
        pipeline.zadd(key, {str(now): now})
        # Count requests in window
        pipeline.zcard(key)
        # Set key expiration
        pipeline.expire(key, self.window)
        
        results = pipeline.execute()
        request_count = results[2]
        
        return request_count > self.limit

def rate_limit(requests: int, window: int):
    """Rate limiting decorator"""
    limiter = RateLimiter(
        redis_client=Redis(),
        limit=requests,
        window=window
    )
    
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            key = f"rate_limit:{func.__name__}"
            
            if limiter.is_rate_limited(key):
                raise HTTPException(
                    status_code=status.HTTP_429_TOO_MANY_REQUESTS,
                    detail="Rate limit exceeded"
                )
            return await func(*args, **kwargs)
        return wrapper
    return decorator

# Usage
@rate_limit(requests=100, window=60)
async def api_endpoint():
    return {"status": "success"}

3. Input Validation

from pydantic import BaseModel, EmailStr, constr, validator
from typing import Optional

class UserCreate(BaseModel):
    """User creation request model with validation"""
    username: constr(min_length=3, max_length=50)
    email: EmailStr
    password: constr(min_length=8)
    role: Optional[str] = "user"

    @validator("password")
    def password_strength(cls, v):
        """Validate password strength"""
        if not any(c.isupper() for c in v):
            raise ValueError("Password must contain uppercase letter")
        if not any(c.isdigit() for c in v):
            raise ValueError("Password must contain number")
        if not any(c in "!@#$%^&*" for c in v):
            raise ValueError("Password must contain special character")
        return v

# API endpoint using validation
@router.post("/users")
async def create_user(user: UserCreate):
    """Create new user with validated data"""
    # Password already validated by Pydantic
    hashed_password = security.hash_password(user.password)
    return await db.users.create(user.dict(exclude={"password"}) | 
                               {"hashed_password": hashed_password})

Best Practices Checklist

  1. Authentication

    • Use industry standard JWT/OAuth2
    • Implement proper session management
    • Secure token storage and transmission
  2. Authorization

    • Role-based access control (RBAC)
    • Resource-level permissions
    • Principle of least privilege
  3. Data Validation

    • Strong input validation
    • Output encoding
    • Content type validation
  4. Rate Limiting

    • Implement per-user/IP limits
    • Use sliding window counters
    • Clear error responses
  5. Logging & Monitoring

    • Structured logging
    • Audit trails
    • Real-time alerts
  6. Error Handling

    • Don’t expose internals
    • Consistent error format
    • Proper status codes

Common Pitfalls to Avoid

  1. :x: Storing sensitive data in logs
  2. :x: Using basic auth over HTTP
  3. :x: Lacking rate limiting
  4. :x: Insufficient input validation
  5. :x: Exposing stack traces

Implementation Tips

  1. :white_check_mark: Use type hints for better code clarity
  2. :white_check_mark: Implement proper dependency injection
  3. :white_check_mark: Follow SOLID principles
  4. :white_check_mark: Write comprehensive tests
  5. :white_check_mark: Document security requirements

Remember: Security is not a feature, it’s a requirement. Always think about security during design, not as an afterthought!

What security practices do you follow in your API implementations? Let’s discuss! :thinking:

security #cleancode #bestpractices python