Every API you expose is a door. Leave it unlocked, and it's only a matter of time before someone walks in uninvited. In 2024 alone, API-related breaches accounted for a significant share of data leaks — from exposed customer records to hijacked payment flows. The stakes are high: regulatory fines, reputational damage, and lost user trust.
Authentication answers "Who are you?" while authorization answers "What are you allowed to do?" Getting both right is non-negotiable. Let's break down the most common approaches and how to implement them properly.
API Keys: Simple but Limited
An API key is a unique string passed with each request, typically in a header or query parameter. The server checks the key against a database of valid keys and grants access accordingly.
How it works:
- Developer registers and receives a key (e.g.,
x-api-key: sk_live_abc123) - Every request includes this key
- Server validates the key and maps it to permissions
Pros:
- Dead simple to implement
- Easy to rotate and revoke
- Great for server-to-server communication
Cons:
- No built-in user identity — a key represents an application, not a user
- If leaked, anyone can use it until revoked
- No expiration by default
When to use: Internal services, rate limiting by client, simple third-party integrations where user context isn't needed.
OAuth 2.0: The Industry Standard for Delegated Access
OAuth 2.0 solves a fundamental problem: letting users grant limited access to their resources without sharing credentials. Instead of handing over a password, users authorize an application through a consent flow and receive scoped tokens.
Authorization Code Flow (for web/mobile apps)
This is the most secure flow for user-facing applications:
- App redirects user to the authorization server
- User logs in and consents
- Authorization server redirects back with a code
- App exchanges the code for an access token (server-side)
- App uses the access token to call the API
The code-to-token exchange happens server-side, so the access token never touches the browser.
Client Credentials Flow (for machine-to-machine)
No user involved — one service authenticates directly with another:
- Service sends its
client_idandclient_secretto the token endpoint - Receives an access token
- Uses it to call the API
When to use OAuth 2.0: Any scenario involving user data, third-party integrations, or when you need scoped, time-limited access.
JWT: Stateless, Portable Tokens
JSON Web Tokens (JWTs) are the most common token format used with OAuth 2.0 and custom auth systems. A JWT has three Base64-encoded parts separated by dots:
header.payload.signature
-
Header: Algorithm and token type (
{"alg": "HS256", "typ": "JWT"}) -
Payload: Claims — user ID, roles, expiration (
{"sub": "user_123", "role": "admin", "exp": 1735689600}) - Signature: HMAC or RSA signature ensuring the token hasn't been tampered with
The server can validate the token without hitting a database — that's the beauty of JWTs. But you must verify the signature and check expiration on every request.
Code Examples in Node.js
JWT Validation Middleware
const jwt = require('jsonwebtoken');
function authenticateJWT(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'your-api.com',
});
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
// Usage
app.get('/api/profile', authenticateJWT, (req, res) => {
res.json({ userId: req.user.sub, role: req.user.role });
});
API Key Authentication Middleware
const crypto = require('crypto');
// Store hashed keys, never plaintext
const validKeys = new Map([
[hashKey('sk_live_abc123'), { name: 'Acme Corp', rateLimit: 1000 }],
]);
function hashKey(key) {
return crypto.createHash('sha256').update(key).digest('hex');
}
function authenticateApiKey(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
const hashedKey = hashKey(apiKey);
const client = validKeys.get(hashedKey);
if (!client) {
return res.status(403).json({ error: 'Invalid API key' });
}
req.client = client;
next();
}
Security Best Practices
Following the right authentication pattern is only half the battle. These practices apply regardless of which method you choose:
- Always use HTTPS. Tokens and keys sent over plain HTTP can be intercepted trivially. There's no excuse in 2026 — TLS certificates are free.
- Validate all input. Never trust client data. Sanitize parameters, validate types, and reject unexpected fields. Use a schema validator like Zod or Joi.
-
Implement rate limiting. Protect against brute-force attacks and abuse. Libraries like
express-rate-limitmake this straightforward. -
Configure CORS properly. Don't use
Access-Control-Allow-Origin: *in production. Whitelist specific origins. - Apply least privilege. Tokens and keys should have the minimum permissions needed. An analytics dashboard doesn't need write access to user data.
- Set token expiration. Short-lived access tokens (15–60 minutes) with refresh tokens strike the right balance between security and UX.
- Store secrets securely. Use environment variables or a secrets manager. Never commit keys to version control.
- Log and monitor. Track failed authentication attempts, unusual access patterns, and token usage. Alerting on anomalies catches breaches early.
Common Mistakes to Avoid
- Storing API keys in plaintext. Always hash keys in your database. If your DB is compromised, plaintext keys give attackers immediate access.
- Not validating JWT signatures. Accepting a JWT without verifying the signature is like checking someone's ID without looking at the photo.
-
Using
alg: none. Some JWT libraries accept unsigned tokens if the algorithm is set tonone. Always specify allowed algorithms explicitly. - Putting sensitive data in JWT payloads. JWTs are encoded, not encrypted. Anyone can decode the payload. Don't include passwords, SSNs, or other secrets.
- Ignoring token revocation. JWTs are stateless, which means a compromised token is valid until it expires. Implement a token blocklist or use short expiration times.
- Hardcoding secrets in source code. It ends up in Git history, CI logs, and error reports. Use environment variables.
- Over-scoping permissions. Granting admin access "just to get it working" and forgetting to restrict it later is how privilege escalation happens.
Wrapping Up
API security isn't a feature you bolt on at the end — it's a foundation you build from day one. Start with the right authentication method for your use case, layer in authorization controls, and follow battle-tested practices. Your future self (and your users) will thank you.
Published by 1xAPI
Top comments (0)