How to Secure Your API Endpoints - A Practical Guide
How to Secure Your API Endpoints — A Practical Guide
APIs power modern applications like mobile apps, single-page apps, microservices, and third-party integrations and when they aren’t secured, they become the easiest attack surface for attackers. A poorly protected API can lead to data leaks, unauthorized access, or even denial-of-service (DoS) attacks that take your service offline.
This guide uses your core points and expands them into a practical, hands-on post you can publish on Blogger. I’ll explain what each method does, why it matters, and show small code examples and best practices.
Why this matters (short)
- Confidentiality: Prevent unauthorized access to user data.
- Integrity: Ensure requests are authentic and not tampered with.
- Availability: Prevent DoS and abusive behavior that drain resources.
Common API Security Methods
-
Authentication (JWT, API Keys, Basic Auth, OAuth 2.0)
-
Authorization (RBAC / ABAC)
-
Use HTTPS / SSL for all communication
-
Input validation (client + server)
-
Rate limiting (throttle abuse)
-
CORS (Cross-Origin Resource Sharing)
-
Secure token storage (client & server)
-
Logging & monitoring
-
API Gateway / WAF
-
API Versioning
1. Authentication — Who is making the request?
JWT (JSON Web Token) — common for user authentication in modern web apps
-
JWT is a self-contained token that carries claims (user ID, role, expiry, etc.).
-
Structure:
header.payload.signature(three dot-separated parts).-
Header: token type & algorithm (e.g., HS256).Payload: claims (userId, role, exp).
-
Signature: HMAC or asymmetric signature over header+payload using a secret/private key.
-
-
Stateless: server does not need to store session state; it validates tokens using the secret/public key.
JWT example (signing in Node.js):
const jwt = require('jsonwebtoken');
const payload = { userId: '123', role: 'user' };
const token = jwt.sign(payload, process.env.SECRET_KEY, { expiresIn: '1h' });
// token -> send back to client
JWT verification (middleware):
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization; // "Bearer <token>"
if (!authHeader) return res.status(401).send('Missing token');
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.SECRET_KEY);
req.user = decoded;
next();
} catch (err) {
res.status(401).send('Invalid or expired token');
}
}
Token expiration
-
Keep tokens short-lived (minutes to an hour) and use refresh tokens (longer lived) to obtain new JWTs.
-
Always store secret keys in environment variables — never in code or a repo.
API Keys — authenticate applications, not users
-
Simple, long random strings issued by a provider (e.g.,
sk-abc123xyz...). -
Sent in headers on each request (e.g.,
Authorization: Bearer <api-key>orx-api-key: ...). -
Best used for server-to-server or application-level authentication.
-
Limitations: generally long-lived and easier to leak if not rotated; do not rely on API keys alone for user-level authorization.
Basic Auth
-
Username/password encoded in Base64 and sent in the header every request.
-
Works OK for internal tools only when used over HTTPS and ideally for simple endpoints. Not recommended for public APIs.
OAuth 2.0
-
Authorization framework for delegated access (login with Google, GitHub, etc.).
-
Provides short-lived access tokens and refresh tokens without saving user passwords.
-
Use when you need third-party login or to grant limited access to another application.
Summary table
| Concept | Authenticates | Use case | Issued by | Expiration |
|---|---|---|---|---|
| API Key | Application | External APIs (OpenAI, Slack) | Provider | Long-lived |
| JWT | User | Your backend | Generated on login | Short-lived (15m–1h) |
| OAuth token | User/app | Third-party login/delegation | OAuth provider | Short-lived + refresh |
-
Authorization — What can the authenticated user do?
Role-Based Access Control (RBAC)
-
Assign roles (admin, user, moderator). Check role before performing sensitive operations.
function requireAdmin(req, res, next) {
if (req.user.role !== 'admin') return res.status(403).send('Forbidden');
next();
}
Attribute-Based Access Control (ABAC)
-
Make decisions using attributes (user department, resource owner, time of day). More flexible for complex policies.
Best practices
-
Deny by default — only allow explicit permissions.
-
Validate both at the API gateway and inside services (defense in depth).
-
Keep authorization logic centralized (or use shared libraries) to avoid inconsistencies.
-
Use HTTPS / SSL — encrypt everything
-
Always serve APIs over HTTPS. This prevents man-in-the-middle attacks and eavesdropping.
-
Redirect HTTP to HTTPS and use HSTS headers where possible.
-
For internal microservices, use mTLS where feasible.
-
Input Validation — never trust user input
-
Client-side validation improves UX, but server-side validation is the security boundary.
-
Common attacks: SQL Injection, XSS, Command Injection.
Client-side validation (example with Zod + React):
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
age: z.number().min(18)
});
const result = userSchema.safeParse(formData);
Server-side validation (Joi example):
const Joi = require('joi');
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
const { error } = schema.validate(req.body);
if (error) return res.status(400).send(error.details[0].message);
Other tips
-
Use prepared statements or ORM parameterization to prevent SQL injection.
-
Use safe HTML templates and escape output to avoid XSS.
-
Limit length and character sets for fields (e.g., usernames, phone numbers).
-
Rate Limiting — stop abuse and mitigate DoS attempts
Express example using express-rate-limit:
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // limit each IP to 100 requests per windowMs
message: "Too many requests, please try again later."
});
app.use('/api/', limiter);
Design choices
-
Rate by IP for public endpoints (simple).
-
Rate by API key or user ID for authenticated APIs (fairer).
-
Consider different limits per route (e.g., sign-in endpoints get stricter limits).
-
Use progressively increasing penalties or backoff rules (e.g., exponential backoff for repeated offenders).
-
CORS — explain simply
A can call an API on domain B. It’s enforced by the browser and configured by the server via response headers.Simple Express example:
import cors from 'cors';
app.use(cors({ origin: 'https://myfrontend.com' }));
Key points
-
Don’t set
Access-Control-Allow-Origin: *for sensitive endpoints. -
Only allow the origins you trust.
-
Preflight requests (OPTIONS) are used by browsers for certain requests — make sure they are handled properly.
-
Secure Token Storage — where to keep secrets and tokens
-
Keep
SECRET_KEY, API keys, database credentials in environment variables (or secret managers like AWS Secrets Manager, Azure Key Vault, HashiCorp Vault). -
Rotate keys periodically and monitor usage.
On the client
-
Never store JWTs or sensitive tokens in plain localStorage if XSS is a risk. Prefer HTTP-only cookies for browsers (prevents JavaScript access) with
SecureandSameSiteattributes. -
Mobile apps should use secure platform storage (iOS Keychain / Android Keystore).
-
Logging & Monitoring — detect and respond
What to log
-
Authentication failures, token validation failures.
-
High-frequency requests or sudden request spikes.
-
Forbidden attempts and unusual resource access.
-
Changes to permissions or admin actions.
Tools
-
CloudWatch (AWS), ELK stack (Elasticsearch / Logstash / Kibana), Datadog, Prometheus + Grafana.
-
Set up alerts for spikes in 4xx/5xx rates or rate-limit events.
Privacy tip: don’t log full tokens, passwords, or sensitive PII. Log identifiers and safe metadata.
-
API Gateway / WAF — a common control plane
-
Authentication & authorization enforcement
-
Rate limiting & throttling
-
Routing & load balancing to backend microservices
-
Request/response transformation, caching
-
Basic DDOS/WAF rules to block common attack patterns
Why use one
-
Centralizes cross-cutting security policies, so each microservice doesn’t need to re-implement them.
-
Simplifies observability (requests pass through a single point).
-
API Versioning — avoid breaking clients
Common strategies
-
URI versioning:
/api/v1/usersand/api/v2/users -
Header versioning:
Accept: application/vnd.myapp.v2+json -
Keep old versions running until consumers migrate; deprecate with clear timelines.
Common mistakes to avoid
-
Storing secret keys in the repo.
-
Using long-lived JWTs without refresh tokens.
-
Allowing
Access-Control-Allow-Origin: *for sensitive APIs. -
Relying only on client-side validation.
-
Failing to rotate API keys or secrets.

Comments
Post a Comment