Application Security
API Security
4 min read
APIs are the backbone of modern applications. This lesson covers authentication patterns, common vulnerabilities, and security best practices you'll encounter in interviews.
Authentication Patterns
API Keys vs OAuth 2.0 vs JWT
| Method | Use Case | Security Level | Management |
|---|---|---|---|
| API Keys | Server-to-server, simple apps | Low (easily leaked) | Manual rotation |
| OAuth 2.0 | Third-party access, user consent | High | Token refresh |
| JWT | Stateless authentication | Medium-High | Signature validation |
Interview Question
Q: "When would you use API keys vs OAuth 2.0?"
Answer:
- API Keys: Internal services, rate limiting identifier, non-sensitive operations
- OAuth 2.0: Third-party integrations, user data access, delegated authorization
- JWT: Mobile apps, microservices, when you need stateless auth with claims
OAuth 2.0 Flows
Authorization Code Flow (Most Secure)
┌─────────┐ ┌──────────────┐
│ User │ │ Auth Server │
└────┬────┘ └──────┬───────┘
│ │
│ 1. Click "Login with Google" │
├────────────────────────────────────────▶│
│ │
│ 2. Redirect to auth server │
│◀────────────────────────────────────────┤
│ │
│ 3. User authenticates & consents │
├────────────────────────────────────────▶│
│ │
│ 4. Redirect back with auth code │
│◀────────────────────────────────────────┤
│ │ │
│ │ 5. Exchange code for tokens │
│ └───────────────────────────────▶│
│ │
│ 6. Return access + refresh tokens│
│◀────────────────────────────────────────┤
PKCE Extension (Required for Public Clients)
import hashlib
import base64
import secrets
# Generate code verifier
code_verifier = secrets.token_urlsafe(32)
# Generate code challenge
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).decode().rstrip('=')
# Use in authorization request
auth_url = f"{AUTH_ENDPOINT}?client_id={CLIENT_ID}&code_challenge={code_challenge}&code_challenge_method=S256"
OWASP API Security Top 10
| Rank | Vulnerability | Example |
|---|---|---|
| API1 | Broken Object Level Authorization | /api/users/123 accessible by user 456 |
| API2 | Broken Authentication | Weak tokens, no rate limiting on login |
| API3 | Broken Object Property Level Authorization | Mass assignment allowing role elevation |
| API4 | Unrestricted Resource Consumption | No pagination limits, memory exhaustion |
| API5 | Broken Function Level Authorization | User accessing admin endpoints |
| API6 | Unrestricted Access to Sensitive Business Flows | Automated ticket scalping |
| API7 | Server Side Request Forgery | Fetch arbitrary URLs via API |
| API8 | Security Misconfiguration | Verbose errors, default credentials |
| API9 | Improper Inventory Management | Undocumented/forgotten API versions |
| API10 | Unsafe Consumption of APIs | Trusting third-party API responses |
Broken Object Level Authorization (BOLA)
# VULNERABLE - No ownership check
@app.get("/api/orders/{order_id}")
def get_order(order_id: int):
return db.query(Order).get(order_id)
# SECURE - Verify ownership
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, current_user: User = Depends(get_current_user)):
order = db.query(Order).get(order_id)
if order.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
return order
Rate Limiting
Implementation Patterns
from fastapi import Request, HTTPException
from datetime import datetime, timedelta
import redis
redis_client = redis.Redis()
def rate_limit(key: str, limit: int, window: int):
"""
Token bucket rate limiting.
Args:
key: Unique identifier (user_id, IP, API key)
limit: Max requests per window
window: Time window in seconds
"""
current = redis_client.get(key)
if current is None:
# First request
redis_client.setex(key, window, 1)
return True
if int(current) >= limit:
raise HTTPException(
status_code=429,
detail="Rate limit exceeded",
headers={"Retry-After": str(window)}
)
redis_client.incr(key)
return True
# Usage
@app.get("/api/search")
def search(request: Request, q: str):
rate_limit(f"search:{request.client.host}", limit=100, window=60)
return perform_search(q)
Input Validation
from pydantic import BaseModel, Field, validator
import re
class UserCreateRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=30)
email: str = Field(..., max_length=255)
password: str = Field(..., min_length=12)
@validator('username')
def username_alphanumeric(cls, v):
if not re.match(r'^[a-zA-Z0-9_]+$', v):
raise ValueError('Username must be alphanumeric')
return v
@validator('email')
def email_valid(cls, v):
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', v):
raise ValueError('Invalid email format')
return v.lower()
@validator('password')
def password_strong(cls, v):
if not re.search(r'[A-Z]', v):
raise ValueError('Password must contain uppercase')
if not re.search(r'[a-z]', v):
raise ValueError('Password must contain lowercase')
if not re.search(r'\d', v):
raise ValueError('Password must contain digit')
return v
Security Headers
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["https://trusted-domain.com"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Authorization", "Content-Type"],
)
@app.middleware("http")
async def add_security_headers(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'"
return response
Interview Tip: When discussing API security, always mention the principle of defense in depth: authentication at the gateway, authorization at the service level, and input validation at every endpoint.
Next, we'll cover DevSecOps and CI/CD security. :::