Security knowledge is increasingly expected in senior engineering interviews — not just for security engineers. This guide covers the API security questions you will encounter at companies like Stripe, Cloudflare, and any company handling sensitive data.
Authentication vs Authorization
"""
Authentication (AuthN): who are you? (identity verification)
Methods: password, API key, OAuth token, mTLS certificate, SAML, WebAuthn
Authorization (AuthZ): what can you do? (permission check)
Models:
RBAC (Role-Based): roles have permissions; users have roles
ABAC (Attribute-Based): policies on user/resource/environment attributes
ReBAC (Relationship-Based): access based on graph (Google Zanzibar)
Scope-based: OAuth scopes define what a token can access
"""
import jwt
import bcrypt
import secrets
import time
from typing import Optional
class AuthService:
JWT_ALGORITHM = "RS256" # Asymmetric: private key signs, public key verifies
ACCESS_TOKEN_TTL = 900 # 15 minutes
REFRESH_TOKEN_TTL = 86400 * 30 # 30 days
def __init__(self, private_key: bytes, public_key: bytes, db):
self.private_key = private_key
self.public_key = public_key
self.db = db
def hash_password(self, password: str) -> str:
"""bcrypt with work factor 12 — ~250ms, too slow to brute-force."""
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)).decode()
def verify_password(self, password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode(), hashed.encode())
def issue_tokens(self, user_id: str, scopes: list) -> dict:
now = int(time.time())
access_payload = {
"sub": user_id,
"iat": now,
"exp": now + self.ACCESS_TOKEN_TTL,
"scope": " ".join(scopes),
"jti": secrets.token_hex(16), # JWT ID for revocation
}
access_token = jwt.encode(access_payload, self.private_key, algorithm=self.JWT_ALGORITHM)
# Refresh token: opaque random token stored in DB (not JWT)
refresh_token = secrets.token_urlsafe(32)
self.db.store_refresh_token(
user_id=user_id,
token_hash=bcrypt.hashpw(refresh_token.encode(), bcrypt.gensalt()).decode(),
expires_at=now + self.REFRESH_TOKEN_TTL,
)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "Bearer",
"expires_in": self.ACCESS_TOKEN_TTL,
}
def verify_access_token(self, token: str) -> Optional[dict]:
try:
payload = jwt.decode(token, self.public_key, algorithms=[self.JWT_ALGORITHM])
# Check revocation list for jti (if revocation needed)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
OAuth 2.0 Flows
"""
OAuth 2.0 Grant Types:
Authorization Code + PKCE (recommended for web/mobile apps):
1. App generates code_verifier (random), code_challenge = SHA256(verifier)
2. Redirect to /authorize?code_challenge=...&client_id=...&scope=...
3. User authenticates at auth server
4. Auth server redirects to app with ?code=...
5. App POSTs /token with code + code_verifier (no client secret needed)
6. Auth server verifies SHA256(verifier) == challenge, returns tokens
PKCE prevents authorization code interception attacks.
Client Credentials (machine-to-machine):
App sends client_id + client_secret to /token
Gets access token for its own resources (no user involved)
Use: service-to-service API calls, cron jobs, CI/CD pipelines
Device Code (TV/CLI apps):
Device shows code + URL on screen
User visits URL on phone, enters code, approves
Device polls /token until approved
Implicit (deprecated — do not use):
Token returned in URL fragment → exposed to browser history, logs
"""
import hashlib
import base64
import secrets
def generate_pkce_pair():
"""Generate PKCE code_verifier and code_challenge."""
code_verifier = secrets.token_urlsafe(64) # 43-128 chars of Base64URL
digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
return code_verifier, code_challenge
# Use in SPA:
# verifier, challenge = generate_pkce_pair()
# Store verifier in sessionStorage (NOT localStorage — XSS risk)
# Pass challenge in authorization request
# Exchange code for token using verifier
OWASP API Security Top 10 (2023)
| # | Vulnerability | Example + Mitigation |
|---|---|---|
| 1 | Broken Object Level Authorization | GET /orders/12345 returns another user’s order. Fix: always check resource ownership in handler. |
| 2 | Broken Authentication | Weak JWT secret, no expiry, tokens not invalidated on logout. Fix: RS256, short TTL, refresh token rotation. |
| 3 | Broken Object Property Level Auth | PATCH /users/me allows updating role field. Fix: explicit allowlist of updatable fields. |
| 4 | Unrestricted Resource Consumption | No rate limit → DoS. Fix: rate limit per IP + per API key; size limits on request body. |
| 5 | Broken Function Level Authorization | POST /admin/delete accessible to regular users. Fix: middleware checks role before routing. |
| 6 | Unrestricted Access to Sensitive Business Flows | No limit on checkout attempts → carding attacks. Fix: business-logic rate limits, CAPTCHA on abuse patterns. |
| 7 | Server Side Request Forgery (SSRF) | API fetches user-supplied URL → hits internal metadata service. Fix: allowlist valid domains; block RFC 1918 ranges. |
| 8 | Security Misconfiguration | Debug endpoints in prod, verbose errors, CORS wildcard. Fix: automated security headers, env-specific config. |
| 9 | Improper Inventory Management | Old API v1 endpoints not deprecated → unpatched. Fix: API versioning strategy with sunset dates. |
| 10 | Unsafe Consumption of APIs | Trusting third-party data without validation. Fix: validate and sanitize all external data. |
Input Validation and Injection Prevention
from pydantic import BaseModel, validator, Field
from typing import Optional
import re
# ALWAYS validate at API boundary — never trust client data
class CreateOrderRequest(BaseModel):
product_id: str = Field(..., regex=r"^[a-zA-Z0-9-]{8,36}$") # UUID-like
quantity: int = Field(..., ge=1, le=1000) # Between 1 and 1000
note: Optional[str] = Field(None, max_length=500)
@validator("note")
def sanitize_note(cls, v):
if v is None:
return v
# Strip HTML tags — prevent stored XSS
v = re.sub(r"]+>", "", v)
return v.strip()
# SQL injection: ALWAYS use parameterized queries
def get_user(conn, user_id: str):
# WRONG: f"SELECT * FROM users WHERE id = {user_id}"
# RIGHT:
cursor = conn.execute("SELECT * FROM users WHERE id = %s", (user_id,))
return cursor.fetchone()
# SSRF prevention
import ipaddress
import urllib.parse
BLOCKED_NETWORKS = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"), # AWS metadata
]
def is_safe_url(url: str) -> bool:
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in ("http", "https"):
return False
try:
addr = ipaddress.ip_address(parsed.hostname)
return not any(addr in net for net in BLOCKED_NETWORKS)
except ValueError:
# Hostname — resolve and check (in production, do this server-side)
return True
Rate Limiting Implementation
import redis
import time
class RateLimiter:
"""
Token bucket algorithm — allows bursting up to capacity,
then refills at steady rate.
"""
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
def is_allowed(self, key: str, capacity: int, refill_rate: float) -> bool:
"""
Returns True if request is allowed.
key: unique identifier (user_id, ip, api_key)
capacity: max burst size
refill_rate: tokens added per second
"""
now = time.time()
bucket_key = f"ratelimit:{key}"
pipe = self.redis.pipeline()
pipe.hgetall(bucket_key)
pipe.time()
results = pipe.execute()
data = results[0]
if data:
tokens = float(data[b"tokens"])
last_refill = float(data[b"last_refill"])
else:
tokens = float(capacity)
last_refill = now
# Calculate tokens to add since last request
elapsed = now - last_refill
tokens = min(capacity, tokens + elapsed * refill_rate)
if tokens bool:
now = int(time.time() * 1000)
window_start = now - window_s * 1000
bucket = f"sw:{key}"
pipe = redis_client.pipeline()
pipe.zremrangebyscore(bucket, "-inf", window_start)
pipe.zadd(bucket, {str(now): now})
pipe.zcard(bucket)
pipe.expire(bucket, window_s + 1)
results = pipe.execute()
count = results[2]
return count <= limit
Companies That Ask This System Design Question
This problem commonly appears in interviews at:
See our company interview guides for full interview process, compensation, and preparation tips.
Frequently Asked Questions
What is the difference between OAuth 2.0 and JWT?
OAuth 2.0 is an authorization framework — it defines how to obtain access tokens. JWT (JSON Web Token) is a token format — a self-contained, signed token. They are complementary: OAuth 2.0 often issues JWTs as access tokens. A JWT contains claims (user ID, scopes, expiry) and is signed by the server, so recipients can verify it without a database lookup.
What is PKCE and why is it needed?
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. The client generates a random code_verifier, sends SHA256(verifier) as code_challenge in the authorization request, then sends the original verifier when exchanging the code for a token. An attacker who intercepts the authorization code cannot use it without the verifier.
What is OWASP API Security Top 10?
OWASP API Security Top 10 (2023) lists the most critical API vulnerabilities: (1) Broken Object Level Authorization, (2) Broken Authentication, (3) Broken Object Property Level Authorization, (4) Unrestricted Resource Consumption, (5) Broken Function Level Authorization, (6) Unrestricted Access to Sensitive Business Flows, (7) SSRF, (8) Security Misconfiguration, (9) Improper Inventory Management, (10) Unsafe Consumption of APIs.
How do you implement rate limiting in an API?
Common algorithms: Token Bucket (allows bursting, refills at steady rate — good for API keys), Sliding Window (counts requests in rolling time window — fair but more memory), Fixed Window (simplest but allows burst at window boundaries). Store counters in Redis with TTL. Rate limit by IP for anonymous, by API key or user ID for authenticated requests.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the difference between OAuth 2.0 and JWT?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “OAuth 2.0 is an authorization framework — it defines how to obtain access tokens. JWT (JSON Web Token) is a token format — a self-contained, signed token. They are complementary: OAuth 2.0 often issues JWTs as access tokens. A JWT contains claims (user ID, scopes, expiry) and is signed by the server, so recipients can verify it without a database lookup.”
}
},
{
“@type”: “Question”,
“name”: “What is PKCE and why is it needed?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. The client generates a random code_verifier, sends SHA256(verifier) as code_challenge in the authorization request, then sends the original verifier when exchanging the code for a token. An attacker who intercepts the authorization code cannot use it without the verifier.”
}
},
{
“@type”: “Question”,
“name”: “What is OWASP API Security Top 10?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “OWASP API Security Top 10 (2023) lists the most critical API vulnerabilities: (1) Broken Object Level Authorization, (2) Broken Authentication, (3) Broken Object Property Level Authorization, (4) Unrestricted Resource Consumption, (5) Broken Function Level Authorization, (6) Unrestricted Access to Sensitive Business Flows, (7) SSRF, (8) Security Misconfiguration, (9) Improper Inventory Management, (10) Unsafe Consumption of APIs.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement rate limiting in an API?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Common algorithms: Token Bucket (allows bursting, refills at steady rate — good for API keys), Sliding Window (counts requests in rolling time window — fair but more memory), Fixed Window (simplest but allows burst at window boundaries). Store counters in Redis with TTL. Rate limit by IP for anonymous, by API key or user ID for authenticated requests.”
}
}
]
}