# API Security

> When to activate: API security, rate limiting, JWT auth, OAuth2, API keys, WAF, GraphQL security, BOLA, broken object level authorization

- Skill: `mattakushi432/api-security` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/api-security`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/api-security/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/api-security

---

# API Security Patterns

## Authentication

```python
# JWT verification — pin algorithm, check expiry
import jwt
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer

security = HTTPBearer()

def verify_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            settings.JWT_PUBLIC_KEY,
            algorithms=["RS256"],   # Pin algorithm — never accept "none"
            options={"require": ["exp", "iat", "sub"]},
            audience="api.example.com",
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "Invalid token")

# API Key — hash stored, compared in constant time
import hashlib, hmac, secrets

def create_api_key() -> tuple[str, str]:
    raw = secrets.token_urlsafe(32)
    hashed = hashlib.sha256(raw.encode()).hexdigest()
    return raw, hashed          # return raw once, store hashed

def verify_api_key(provided: str, stored_hash: str) -> bool:
    provided_hash = hashlib.sha256(provided.encode()).hexdigest()
    return hmac.compare_digest(provided_hash, stored_hash)
```

## Authorization — BOLA Prevention

```python
# Broken Object Level Authorization — always check ownership
from fastapi import Depends

async def get_invoice(
    invoice_id: int,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    invoice = await db.get(Invoice, invoice_id)
    if not invoice:
        raise HTTPException(404)

    # CRITICAL: verify ownership — never skip this
    if invoice.owner_id != current_user.id:
        raise HTTPException(403, "Access denied")   # Not 404 — don't leak existence

    return invoice

# For admin endpoints — verify role server-side (not just in UI)
def require_role(role: str):
    def dependency(user: User = Depends(get_current_user)):
        if role not in user.roles:
            raise HTTPException(403, f"Role {role} required")
        return user
    return dependency
```

## Rate Limiting

```python
# FastAPI with slowapi
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/login")
@limiter.limit("5/minute")          # 5 attempts per minute per IP
async def login(request: Request, credentials: LoginRequest):
    ...

@app.post("/api/send-otp")
@limiter.limit("3/hour")            # Prevent OTP flooding
async def send_otp(request: Request):
    ...
```

```nginx
# Nginx rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;

location /api/ {
    limit_req zone=api burst=20 nodelay;
    limit_req_status 429;
}

location /api/auth/ {
    limit_req zone=auth burst=3 nodelay;
    limit_req_status 429;
}
```

## Input Validation

```python
from pydantic import BaseModel, validator, constr
import re

class CreateUserRequest(BaseModel):
    username: constr(min_length=3, max_length=30, pattern=r'^[a-zA-Z0-9_]+$')
    email: str
    age: int

    @validator('email')
    def validate_email(cls, v):
        if not re.match(r'^[^@]+@[^@]+\.[^@]+$', v):
            raise ValueError('Invalid email format')
        return v.lower().strip()

    @validator('age')
    def validate_age(cls, v):
        if not 0 < v < 150:
            raise ValueError('Invalid age')
        return v
```

## GraphQL Security

```python
# Depth limiting — prevent deeply nested queries
from graphql import build_schema
from graphql_depth_limit import depth_limit_validator

schema = build_schema(type_defs)

# Reject queries deeper than 5 levels
validation_rules = [depth_limit_validator(max_depth=5)]

# Query complexity limiting
from graphql_query_complexity import QualityLimitRule

class ComplexityRule(QualityLimitRule):
    def __init__(self, context):
        super().__init__(context, max_complexity=100)

# Disable introspection in production
GRAPHQL_INTROSPECTION = settings.DEBUG  # False in prod

# Field-level authorization
@strawberry.type
class User:
    name: str

    @strawberry.field
    def email(self, info: Info) -> str:
        if info.context.user.id != self.id and not info.context.user.is_admin:
            raise PermissionError("Cannot view other users' email")
        return self._email
```

## Security Headers

```python
# FastAPI middleware
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware

class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        response = await call_next(request)
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["X-Frame-Options"] = "DENY"
        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.pop("Server", None)    # Don't expose server info
        return response

app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],  # Explicit, never "*"
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)
```

## API Security Checklist

```
Authentication: JWT pins alg, API keys hashed, OAuth PKCE for SPAs
Authorization: BOLA checked server-side on every resource access
Rate Limiting: login ≤5/min, OTP ≤3/hr, general API ≤100/min
Input: schema validation on all inputs, content-type enforced
Output: no stack traces, no internal paths, minimal error details
Transport: HTTPS only, HSTS header, TLS 1.2+ minimum
Headers: security headers set, Server header removed
GraphQL: depth limit, complexity limit, introspection off in prod
Logging: log auth failures, 4xx/5xx, but NOT request bodies with PII
```

