Skip to content

API Security

REST APIs are stateless by design: each request must contain all information needed for Authentication and authorization. The server does not maintain session state between requests.

Every request must include:
1. Authentication credential (Bearer token, API key, mTLS certificate)
2. Required headers (Content-Type, Accept)
3. Any correlation/tracing identifiers
The server validates credentials on every request.
No server-side session is required (but tokens must be stateless or validated).
# Simple API key in header
API_KEYS = {
"key_abc123': {'name': "service-a'', "scopes': ['read:users']},
'key_def456': {'name': "service-b'', "scopes': ['read:users', 'write:orders']},
}
@app.before_request
def validate_api_key():
api_key = request.headers.get('X-API-Key')
if not api_key or api_key not in API_KEYS:
return jsonify({"error": "Invalid API key"}), 401
g.api_key_info = API_KEYS[api_key]

Rate limit by user identity (API key or token) when authenticated, and by IP address when Unauthenticated. Unauthenticated rate limits should be stricter to prevent abuse.

from jsonschema import validate, ValidationError
CREATE_ORDER_SCHEMA = {
"type": "object",
"required": ["customer_id", "items"],
"properties": {
"customer_id": {"type": "integer", "minimum": 1},
"items": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"items": {
"type": "object",
"required": ["product_id", "quantity"],
"properties": {
"product_id": {"type": "integer", "minimum": 1},
"quantity": {"type": "integer", "minimum": 1, "maximum": 999}
}
}
},
"notes": {"type": "string", "maxLength": 1000}
},
"additionalProperties": False
}
@app.route('/api/orders', methods=['POST'])
@require_bearer_token(['write:orders'])
def create_order():
data = request.get_json()
try:
validate(data, CREATE_ORDER_SCHEMA)
except ValidationError as e:
return jsonify({"error": f"Validation failed: {e.message}"}), 400
...
def validate_pagination(request):
"""Validate and sanitize pagination parameters."""
try:
limit = int(request.args.get('limit', 20))
offset = int(request.args.get('offset', 0))
except (ValueError, TypeError):
return None, None, "limit and offset must be integers"
limit = max(1, min(limit, 100)) # Clamp to [1, 100]
offset = max(0, min(offset, 10000)) # Cap at 10000
return limit, offset, None
from flask_cors import CORS
# SAFE: explicit allowlist
ALLOWED_ORIGINS = [
'https://app.example.com',
'https://admin.example.com'
]
CORS(app, origins=ALLOWED_ORIGINS, methods=['GET', 'POST', 'PUT', 'DELETE'],
allow_headers=['Authorization', 'Content-Type'],
supports_credentials=True)
# VULNERABLE: wildcard with credentials
# CORS(app, origins='*', supports_credentials=True) # BLOCKED by browsers
StrategyExampleProsCons
URL path/api/v1/ordersSimple, visibleURL changes
HeaderAccept: application/vnd.api.v1+jsonClean URLsHidden, harder to test
Query parameter/api/orders?version=1Easy to addCache-busting issues
# URL path versioning (recommended for most APIs)
@app.route('/api/v1/orders')
def list_orders_v1():
return jsonify(legacy_format(orders))
@app.route('/api/v2/orders')
def list_orders_v2():
return jsonify(modern_format(orders))
# Sparse fieldsets (JSON:API pattern)
@app.route('/api/v1/users')
def list_users():
fields = request.args.get('fields', '').split(',')
users = get_all_users()
if fields:
filtered = [{k: u[k] for k in fields if k in u} for u in users]
return jsonify(filtered)
return jsonify(users)
# Offset pagination (simple, but slow at high offsets)
@app.route('/api/orders')
def list_orders():
limit = min(int(request.args.get('limit', 20)), 100)
offset = max(int(request.args.get('offset', 0)), 0)
orders = db.query("SELECT * FROM orders ORDER BY id LIMIT %s OFFSET %s", (limit, offset))
return jsonify({"data": orders, "offset": offset, "limit": limit})
# Cursor pagination (efficient at any position)
@app.route('/api/orders')
def list_orders():
limit = min(int(request.args.get('limit', 20)), 100)
cursor = request.args.get('cursor')
if cursor:
orders = db.query(
"SELECT * FROM orders WHERE id > %s ORDER BY id LIMIT %s",
(cursor, limit)
)
else:
orders = db.query(
"SELECT * FROM orders ORDER BY id LIMIT %s",
(limit,)
)
next_cursor = orders[-1]['id'] if orders else None
return jsonify({
"data": orders,
"next_cursor": next_cursor
})
MethodPerformance at page 10000Consistency on inserts/deletesURL bookmarkable
OffsetSlow (scans 10000+ rows)Unstable (rows shift)Yes
CursorFast (index lookup)Stable (deterministic)No (cursor changes)
from hashlib import sha256
@app.route('/api/orders', methods=['POST'])
@require_bearer_token(['write:orders'])
def create_order():
idempotency_key = request.headers.get('Idempotency-Key')
if not idempotency_key:
return jsonify({"error": "Idempotency-Key header required"}), 400
# Check if this key was already processed
existing = redis.get(f"idempotency:{idempotency_key}")
if existing:
return jsonify(json.loads(existing)), 200 # Return original response
# Process the order
order = create_order_from_request(request)
response = jsonify(order)
# Store the response for future retries (TTL = 24h)
redis.setex(
f"idempotency:{idempotency_key}",
86400,
json.dumps(order)
)
return response, 201
import hmac
import hashlib
def verify_webhook_signature(request, secret):
signature = request.headers.get('X-Webhook-Signature')
if not signature:
return False
payload = request.get_data()
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhooks/payment', methods=['POST'])
def payment_webhook():
if not verify_webhook_signature(request, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
event = request.get_json()
process_payment_event(event)
return jsonify({"status": "ok"}), 200
API Gateway responsibilities:
1. Authentication (validate tokens, API keys)
2. Rate limiting (per-client, per-route)
3. Request transformation (headers, body)
4. Response transformation (filtering, formatting)
5. Load balancing (round-robin, least connections)
6. Circuit breaking (fail fast on downstream failures)
7. Request logging and monitoring
8. TLS termination
OptionComplexityCustomizationUse Case
KongMediumHigh (Lua plugins)Large-scale, extensible
AWS API GatewayLowMediumAWS ecosystem
EnvoyHighVery HighService mesh, gRPC
NginxMediumHighGeneral purpose, lightweight
TraefikLowMediumContainer environments
# Limit maximum query depth to prevent complex/nested queries
MAX_QUERY_DEPTH = 5
def validate_query_depth(query_ast, current_depth=0):
if current_depth > MAX_QUERY_DEPTH:
raise GraphQLDepthError(f"Query depth exceeds maximum of {MAX_QUERY_DEPTH}")
for field in query_ast.selection_set.selections:
if hasattr(field, 'selection_set') and field.selection_set:
validate_query_depth(field, current_depth + 1)
# Assign cost to each field and limit total query complexity
FIELD_COSTS = {
'orders': 10,
'items': 5,
'customer': 1,
}
MAX_COMPLEXITY = 500
def calculate_complexity(query_ast):
total = 0
for field in query_ast.selection_set.selections:
field_name = field.name.value
cost = FIELD_COSTS.get(field_name, 1)
if hasattr(field, 'selection_set') and field.selection_set:
cost *= calculate_complexity(field)
total += cost
return total
// Disable introspection in production
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
});
# openapi.yaml security schemes
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
tokenUrl: https://auth.example.com/token
scopes:
read:orders: Read orders
write:orders: Create/update orders
security:
- BearerAuth: []
- ApiKeyAuth: []
paths:
/orders:
get:
security:
- BearerAuth: [read:orders]
post:
security:
- OAuth2: [write:orders]

Client-side validation improves UX but provides zero security. An attacker can send any payload Directly to your API. Always validate on the server, regardless of what the client does.

Reflecting the Origin header without validation allows any origin to make authenticated requests. Always use an explicit allowlist.

Using sequential integer IDs (1, 2, 3…) allows attackers to enumerate resources. Use UUIDs or Hashids for public identifiers.

Not Rate Limiting Authentication Endpoints

Section titled “Not Rate Limiting Authentication Endpoints”

Without rate limiting, attackers can brute-force credentials, enumerate usernames, and perform Credential stuffing attacks. Rate limit all authentication endpoints.

Stack traces reveal implementation details (framework, library versions, file paths) that help Attackers craft targeted exploits. Return generic error messages in production; log details Server-side.

This topic covers the essential concepts and techniques related to api security, including key principles and practical applications.

Key concepts include:

  • core concepts and definitions
  • key principles and frameworks
  • practical applications
  • common techniques and methods
  • evaluation and critical analysis

A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.