API Security Best Practices: A Comprehensive Guide
APIs are the front door to your application. Every public endpoint is an attack surface. Poor API security leads to data breaches, account takeovers, and service outages — all of which erode user trust and invite regulatory penalties.
This guide covers the OWASP API Security Top 10, practical authentication and authorization patterns, and the defensive layers every API should have.
OWASP API Security Top 10#
The OWASP API Security project identifies the most critical risks:
- Broken Object-Level Authorization (BOLA) — attackers manipulate IDs to access other users' resources.
- Broken Authentication — weak or missing authentication lets attackers impersonate users.
- Broken Object Property-Level Authorization — exposing or accepting sensitive fields the caller should not control.
- Unrestricted Resource Consumption — no rate limits or size caps enable DoS.
- Broken Function-Level Authorization — regular users access admin endpoints.
- Unrestricted Access to Sensitive Business Flows — automated abuse of checkout, signup, or booking flows.
- Server-Side Request Forgery (SSRF) — the API fetches attacker-controlled URLs.
- Security Misconfiguration — verbose errors, default credentials, missing headers.
- Improper Inventory Management — old or undocumented endpoints remain exposed.
- Unsafe Consumption of APIs — trusting data from third-party APIs without validation.
Use this list as a checklist during design reviews and penetration testing.
Authentication#
API Keys#
A shared secret sent in a header (X-API-Key). Simple to implement, but keys are long-lived, hard to scope, and easy to leak in logs or client-side code. Use API keys only for server-to-server calls where you control both sides.
OAuth 2.0#
The industry standard for delegated authorization. The client obtains a short-lived access token from an authorization server and sends it with each request.
Client → Auth Server (credentials) → Access Token
Client → API (Bearer token) → Protected Resource
Use the Authorization Code flow with PKCE for public clients (SPAs, mobile apps). Use the Client Credentials flow for machine-to-machine calls.
JWT (JSON Web Tokens)#
JWTs carry claims signed by the issuer. The API verifies the signature without hitting a database on every request.
// Verify a JWT with a public key
const decoded = jwt.verify(token, publicKey, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "https://api.example.com",
});
Best practices for JWTs:
- Use asymmetric signing (RS256 or ES256), not HS256 with a shared secret.
- Set short expiration times (5–15 minutes) and use refresh tokens.
- Validate
iss,aud,exp, andnbfclaims. - Never store sensitive data in the payload — it is base64-encoded, not encrypted.
Authorization#
Role-Based Access Control (RBAC)#
Assign users to roles (admin, editor, viewer) and grant permissions to roles. Simple and widely understood.
PERMISSIONS = {
"admin": ["read", "write", "delete", "manage_users"],
"editor": ["read", "write"],
"viewer": ["read"],
}
Attribute-Based Access Control (ABAC)#
Evaluate policies based on user attributes, resource attributes, and context. More flexible than RBAC but harder to audit.
ALLOW if user.department == resource.department
AND user.clearance >= resource.classification
AND time.hour BETWEEN 9 AND 17
ABAC shines when authorization logic depends on dynamic context — location, time, data sensitivity.
Object-Level Checks#
Regardless of the model, always verify that the authenticated user has access to the specific resource. This is the most common gap (OWASP #1).
# Always check ownership
task = db.get_task(task_id)
if task.owner_id != current_user.id:
raise ForbiddenError("Access denied")
Input Validation#
Never trust client input. Validate on the server side even if the client validates too.
- Schema validation — use JSON Schema, Zod, or OpenAPI request validation middleware.
- Type and range checks — reject unexpected types, negative amounts, oversized strings.
- Allowlists over denylists — explicitly permit known-good values rather than blocking known-bad ones.
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100),
role: z.enum(["viewer", "editor"]),
});
Rate Limiting#
Rate limiting protects against brute-force attacks, credential stuffing, and accidental traffic spikes.
- Fixed window — allow N requests per time window.
- Sliding window — smoother distribution, avoids burst at window boundaries.
- Token bucket — allows short bursts while enforcing an average rate.
Return 429 Too Many Requests with a Retry-After header. Apply stricter limits to sensitive endpoints like login and password reset.
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 23
X-RateLimit-Reset: 1711584000
CORS (Cross-Origin Resource Sharing)#
Misconfigured CORS is a common vulnerability. Never set Access-Control-Allow-Origin: * on authenticated endpoints.
- Allowlist specific origins.
- Restrict allowed methods and headers.
- Avoid reflecting the
Originheader without validation.
app.use(cors({
origin: ["https://app.example.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true,
}));
HTTPS and TLS#
Every API must use HTTPS. Period. Without TLS, tokens, credentials, and data travel in plaintext.
- Use TLS 1.2 or 1.3 — disable older versions.
- Enable HSTS (
Strict-Transport-Security) to prevent downgrade attacks. - Pin certificates in mobile apps to prevent MITM with rogue CAs.
- Terminate TLS at the load balancer or API gateway, not deep inside the network.
API Gateway Security#
An API gateway centralizes cross-cutting security concerns:
- Authentication — validate tokens before requests reach backend services.
- Rate limiting — enforce limits at the edge.
- IP allowlisting/blocklisting — block known bad actors.
- Request/response transformation — strip internal headers, redact sensitive fields.
- WAF integration — detect SQL injection, XSS, and other attack patterns.
Tools like Kong, AWS API Gateway, and Envoy provide these capabilities out of the box.
Preventing SQL Injection and XSS#
SQL Injection#
Always use parameterized queries. Never interpolate user input into SQL strings.
# Vulnerable
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# Safe — parameterized
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
ORMs like SQLAlchemy, Prisma, and Sequelize parameterize by default, but raw query escape hatches remain dangerous.
Cross-Site Scripting (XSS)#
If your API returns HTML or is consumed by a browser:
- Encode output contextually (HTML, JavaScript, URL).
- Set
Content-Type: application/jsonon JSON endpoints so browsers do not interpret responses as HTML. - Use
Content-Security-Policyheaders to restrict script sources.
Logging and Monitoring#
You cannot secure what you cannot see. Log every authentication event, authorization failure, and anomalous request pattern.
- Structured logging — use JSON logs with correlation IDs for traceability.
- Alerting — trigger alerts on spikes in 401/403/429 responses.
- Audit trail — log who accessed what resource and when, for compliance.
- Avoid logging secrets — never log tokens, passwords, or full credit card numbers.
{
"timestamp": "2026-03-28T14:30:00Z",
"level": "warn",
"event": "auth_failure",
"ip": "203.0.113.42",
"path": "/api/v1/admin/users",
"reason": "insufficient_role"
}
Integrate with SIEM tools (Datadog, Splunk, ELK) for centralized analysis and incident response.
Security Checklist#
- All endpoints require authentication unless explicitly public.
- Object-level authorization checks on every request.
- Input validated with schemas on the server side.
- Rate limiting on all endpoints, stricter on auth flows.
- CORS configured with explicit origin allowlist.
- HTTPS enforced with TLS 1.2+.
- Secrets rotated regularly, never logged.
- Monitoring and alerting on security events.
Conclusion#
API security is not a feature you bolt on at the end — it is a design discipline that starts at architecture and runs through every layer. Follow the OWASP API Top 10 as your baseline, implement defense in depth, and monitor continuously.
At codelit.io we build tools that help engineering teams ship secure software faster. Try Codelit today.
This is article #160 in our engineering blog series.
Try it on Codelit
GitHub Integration
Paste any repo URL to generate an interactive architecture diagram from real code
Related articles
Try these templates
Build this architecture
Generate an interactive architecture for API Security Best Practices in seconds.
Try it in Codelit →
Comments