Overview
API security is a discipline where a mistake usually costs someone other than the person who made it: exposed user data, a compromised partner system, or damage to your company's reputation and legal standing. The upside is that the core defenses aren't mysterious. Generate keys with enough entropy, store credentials as hashes rather than raw values, keep tokens short-lived, encrypt every connection, and validate whatever comes through the door.
This guide walks through six practices that cover most real-world API security incidents. Each step explains not only what to do but which specific parameters to use in 2026, and the tools linked along the way let you generate keys, validate JWTs, test hash functions, and check certificate setups right in your browser, no extra software required.
Security isn't something you configure once and forget. Threats shift, libraries pick up new vulnerabilities, and even a securely generated credential eventually needs rotating. The monitoring and rotation habits in Step 6 matter as much as the setup work in Steps 1 through 5. A system configured perfectly on day one but never watched afterward will get compromised eventually.
Step 1: Generate and Manage API Keys
API keys are usually the first credential a developer implements and the one thought through least carefully. A poorly generated key is about as dangerous as no key at all if it can be guessed or enumerated.
Entropy requirements:
Generate a secure API key from a cryptographically secure random number generator (CSPRNG), not Math.random(), not UUID v4, and not anything seeded from a timestamp. The floor is 32 bytes, or 256 bits, of random data.
In practice:
// Node.js (correct)
const crypto = require('crypto');
const apiKey = crypto.randomBytes(32).toString('hex'); // 64-char hex string
// Python (correct)
import secrets
api_key = secrets.token_hex(32) # 64-char hex string
// Wrong, do NOT use
const apiKey = uuid.v4(); // Only 122 bits of entropy, structured format
Key structure and prefixes:
Adopt a prefixed format so keys are identifiable at a glance and secret-scanning tools can catch them if they leak into logs or repos:
sk_live_a3f9c2e8b1d4... (secret key, live environment)
pk_live_7b2d1a9e4c3f... (publishable key, live)
sk_test_f1e8d2c9b5a4... (secret key, test environment)
The prefix also lets you log or display a partial key safely for debugging, without exposing the working credential.
Use the API Key Generator to generate properly structured, high-entropy keys instantly for development and testing.
Storage and lookup:
Never store the raw key. The pattern that works:
- Generate the key and show it to the user once, and only once.
- Compute its SHA-256 hash.
- Store
{ key_prefix: "sk_live_a3f9c2", key_hash: "<sha256>", created_at, expires_at, scopes }. - On each request, look up by prefix, hash the presented key, and compare in constant time.
Constant-time comparison matters here. Use crypto.timingSafeEqual() in Node.js or hmac.compare_digest() in Python so a timing side-channel can't leak information about the correct hash.
Step 2: Implement Authentication with JWT
JWT (JSON Web Token) is a compact way to pass authentication claims between parties. It's everywhere in API authentication, and also frequently implemented in ways that open real vulnerabilities.
Token structure:
A JWT has three Base64URL-encoded parts joined by dots: Header.Payload.Signature. The header names the algorithm, the payload holds claims like sub (subject or user ID), exp (expiry), iss (issuer), and aud (audience), and the signature confirms nothing's been tampered with.
Algorithm choice:
| Algorithm | Type | Recommended Use |
|---|---|---|
| RS256 | Asymmetric (RSA) | Recommended for distributed systems |
| ES256 | Asymmetric (ECDSA) | Recommended, smaller keys than RS256 |
| HS256 | Symmetric (HMAC) | Only for single-service or monolith scenarios |
| none | No signature | Never, see the alg:none vulnerability below |
Reach for RS256 or ES256 whenever multiple services verify tokens but only the auth server should be able to sign them. Verifying services only get the public key under these schemes, so they can confirm a token's valid without ever gaining the power to forge one.
Token lifetime:
- Access token: 15 to 60 minutes at most, short enough that a stolen token expires before anyone can put it to real use.
- Refresh token: 7 to 30 days, kept in an HttpOnly, Secure cookie rather than localStorage, which JavaScript (and therefore XSS) can read.
- Refresh tokens should rotate on use: each one issued invalidates the last.
Mandatory validation checks:
Before trusting a JWT, check every one of these:
- The alg header matches what you expect; reject none outright.
- The signature verifies against the correct key.
- exp is still in the future.
- nbf, if present, is already in the past.
- iss matches your expected issuer.
- aud matches your service identifier.
Use the JWT Validator to inspect and validate tokens during development, debugging, or an incident investigation.
Step 3: Hash Secrets and Passwords Properly
The most damaging credential-storage mistake is saving a password or secret in a form that can be reversed or cracked once a database leaks.
Password hashing, and why it needs to be slow:
General-purpose hash functions like SHA-256, SHA-1, and MD5 are built for speed, and speed is the wrong property for a password hash: it lets an attacker holding a leaked hash try billions of guesses per second.
The right algorithms for passwords:
| Algorithm | Parameters (2026) | Notes |
|---|---|---|
| bcrypt | cost factor ≥ 12 | Widely supported, about 250ms at factor 12 |
| Argon2id | 64MB memory, 3 iterations | Current OWASP recommendation |
| scrypt | N=32768, r=8, p=1 | Strong, slightly less library support |
// bcrypt in Node.js
const bcrypt = require('bcrypt');
const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);
// Verify
const match = await bcrypt.compare(inputPassword, storedHash);
Cost factor 12 adds roughly 250ms per hash, which a user logging in once won't notice but which multiplies an attacker's offline cracking time by 2^12 compared to unsalted SHA-256.
API keys and token hashing: SHA-256 is fine here:
For API keys, session tokens, and password reset tokens (not passwords), SHA-256 works well. These strings already carry high entropy, and you need deterministic lookups when a key comes in, which a slow algorithm would make painfully slow. Salting adds nothing once a secret already has 256 bits of entropy; a rainbow table for values that large simply isn't feasible to build.
Use the Hash Generator to test hash functions, check expected outputs, and compare algorithms while you build.
Salting:
A salt is a random value mixed into a password before hashing, so two users with the same password end up with different stored hashes. Bcrypt, Argon2id, and scrypt all generate and store this salt for you automatically. Skip any scheme that relies on a fixed "pepper" without a random salt underneath it.
Step 4: Enforce HTTPS and Certificate Hygiene
All API traffic needs to run over TLS (Transport Layer Security). Sending credentials, tokens, or user data over plain HTTP in 2026 isn't a defensible choice.
Minimum TLS requirements:
- Require TLS 1.2 or 1.3, and turn off TLS 1.0 and 1.1 entirely; both carry known protocol flaws (POODLE, BEAST, CRIME among them).
- Prefer TLS 1.3 wherever you can: it strips out weak cipher suites at the protocol level and provides forward secrecy by default.
- Configure strong cipher suites. In Nginx:
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305;
HSTS (HTTP Strict Transport Security):
Add the HSTS header so browsers and HTTP clients never fall back to plain HTTP for your domain:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Start with max-age=300 (five minutes) to confirm it works, then raise it to 31536000 (one year). Submit your domain to the HSTS preload list at hstspreload.org so browsers enforce it before the very first connection.
Certificate management:
- Use a 2048-bit RSA or 256-bit ECDSA certificate. ECDSA is the better pick: smaller, faster, and just as secure.
- Automate renewal, whether through Let's Encrypt and certbot, AWS ACM, or Caddy's built-in ACME support. Manual renewal is a reliability risk waiting to happen.
- For internal services or client certificate auth, the CSR Generator generates certificate signing requests correctly.
- If you're pinning certificates in a mobile app, pin the intermediate CA, not the leaf certificate, so routine leaf rotation doesn't break the app.
Redirect HTTP to HTTPS at the infrastructure level (load balancer or reverse proxy), not inside the application. An application-level redirect fails the moment the application crashes; the infrastructure one doesn't.
Step 5: Validate All Inputs and Rate Limit
Input validation and rate limiting aren't secondary concerns tacked on at the end. They're what stands between your API and injection attacks, credential-stuffing brute force, and denial-of-service through resource exhaustion.
Input validation principles:
Use an allowlist: define exactly what valid input looks like and reject anything that doesn't match.
// Whitelist validation example
function validateApiKeyRequest(body) {
const schema = {
name: { type: 'string', maxLength: 100, pattern: /^[a-zA-Z0-9 _-]+$/ },
scopes: { type: 'array', items: ['read', 'write', 'admin'], maxItems: 10 },
expiresAt: { type: 'date', min: new Date(), max: addDays(new Date(), 365) }
};
// Reject any field not in schema (strip unknown fields)
// Reject any value that doesn't match type/pattern/bounds
}
A few rules worth following:
- Enforce type on every field and reject mismatches.
- Cap the max length on every string field to head off memory exhaustion.
- Validate format, email, URL, date, before you process anything.
- Strip or reject fields you don't recognize rather than passing them through.
- Sanitize before rendering, not before storing; XSS is a rendering problem, not a storage one.
Rate limiting:
Apply it at two levels. Per API key, cap request volume over a sliding window, say 1,000 requests per minute, to stop one key from exhausting resources on its own. Per IP address, limit unauthenticated requests and login attempts, say 10 per hour, to blunt credential stuffing.
When a limit trips, respond with:
HTTP 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1751000000
Put rate limiting at the API gateway or reverse proxy layer (Nginx, Kong, AWS API Gateway), not only in the application, which someone can bypass by hitting the infrastructure directly.
Set maximum request body size and URL length at the load balancer too. It's a separate control from rate limiting, but it closes off oversized-payload attacks the same way.
Step 6: Monitor, Audit, and Rotate
An API secured perfectly on day one is only as safe as the first anomaly nobody notices. Logging, alerting, and routine rotation are what separate catching a breach within hours from finding out about it months later.
What to log on every request:
{
"timestamp": "2026-06-26T09:14:32.000Z",
"requestId": "req_f3a9b2c1",
"keyPrefix": "sk_live_a3f9c2",
"ipAddress": "103.21.244.0",
"region": "IN-MH",
"method": "POST",
"path": "/v1/payments",
"statusCode": 200,
"latencyMs": 87
}
Never log the full API key, passwords, personal fields like email or phone in request bodies, or card numbers. The key prefix (first 6-8 characters) gives you traceability without exposing anything usable.
Alerting triggers:
Set alerts for a few specific patterns: a key that's only ever run from Mumbai suddenly making requests from a European IP; volume from a key spiking between 2 and 5 AM local time well past its 30-day baseline; more than 10 consecutive 401 or 403 responses from one IP within 5 minutes; overall 5xx rate above 1% for more than 3 minutes; and a key that normally hits read endpoints suddenly reaching for write or admin ones.
Rotation schedule:
| Credential Type | Recommended Rotation | Trigger for Immediate Rotation |
|---|---|---|
| API keys (user-facing) | Annually or on role change | Any suspected exposure |
| API keys (service-to-service) | Every 90 days | Any suspected exposure |
| JWT signing keys (symmetric) | Every 90 days | Any compromise |
| JWT signing keys (asymmetric) | Annually | Any compromise |
| TLS certificates | At expiry (automate) | Any CA compromise |
Rotate with a grace period: generate the new credential, share it, run both in parallel for 24 to 72 hours, confirm the new one's actually in use, then revoke the old one. That avoids the downtime an instant swap would cause.
Keep a revocation record (a revoked_at timestamp on the key row is enough) and check it on every request. A revoked key should return 401 immediately, with a message telling the developer to generate a new one.
Key Terms
JWT (JSON Web Token): a compact, URL-safe token made up of a header, payload, and signature, used to carry authentication claims between parties in a stateless API.
API Key: a unique credential string an API provider issues to identify and authenticate a client or service. Treat it as a secret; never expose it in client-side code.
Hash: the output of a one-way function that turns input of any length into a fixed-length digest. The same input always hashes the same way, and there's no reversing a hash back to its original input.
Salt: a random value mixed into a password before hashing, so two users with identical passwords get different stored hashes, which blocks rainbow table attacks.
TLS (Transport Layer Security): the cryptographic protocol behind encrypted, authenticated network communication, and the successor to the deprecated SSL. API traffic should run on TLS 1.2 or 1.3.
HTTPS: HTTP carried over a TLS connection, giving you encryption in transit. Required for any production API or any service handling credentials or user data.
Rate Limiting: a control that caps how many requests a client can make in a given window, protecting an API from abuse, brute force, and denial-of-service.
HSTS (HTTP Strict Transport Security): a response header that tells clients to connect to a domain only over HTTPS, closing off protocol downgrade attacks.
Entropy: a measure of randomness in a value, in bits. More entropy means more possible values, and more resistance to guessing.