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

  1. Authentication (JWT, API Keys, Basic Auth, OAuth 2.0)

  2. Authorization (RBAC / ABAC)

  3. Use HTTPS / SSL for all communication

  4. Input validation (client + server)

  5. Rate limiting (throttle abuse)

  6. CORS (Cross-Origin Resource Sharing)

  7. Secure token storage (client & server)

  8. Logging & monitoring

  9. API Gateway / WAF

  10. API Versioning


1. Authentication — Who is making the request?
    
Authentication answers the question: Who is making the request? There are multiple techniques, each with different trade-offs.

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> or x-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

  1. Authorization — What can the authenticated user do?

Once a user (or app) is authenticated, you must control what they’re permitted to 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.


  1. 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.


  1. 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).


  1. Rate Limiting — stop abuse and mitigate DoS attempts

Rate limiting restricts how many requests an IP/user/client can make within a time window. This protects backend resources and reduces the blast radius of abusive clients or buggy clients stuck in loops.

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).


  1. CORS — explain simply

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls if a webpage from domain 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.


  1. Secure Token Storage — where to keep secrets and tokens

       On the server
  • 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 Secure and SameSite attributes.

  • Mobile apps should use secure platform storage (iOS Keychain / Android Keystore).


  1. Logging & Monitoring — detect and respond

Logging and monitoring help you detect suspicious patterns and respond quickly.

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.


  1. API Gateway / WAF — a common control plane

An API Gateway (e.g., AWS API Gateway, Kong, NGINX) or a Web Application Firewall (WAF) performs centralized functions:
  • 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).


  1. API Versioning — avoid breaking clients

When API changes are needed (new fields, renames, behavior changes), version your API to prevent breaking existing clients.

Common strategies

  • URI versioning: /api/v1/users and /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

Popular posts from this blog

Mastering SQL Essentials: Injection, Indexing, Views, Stored Procedures & Triggers

Why "Inglourious Basterds" is Tarantino’s Best, As I See It