Understanding the Landscape
When building modern APIs, choosing the right authentication mechanism is critical. As your application scales, basic checks are no longer sufficient. You must design for revocation, scoped access, and leak prevention.
1. API Keys (Machine-to-Machine)
API keys are opaque strings passed with every request, usually in an HTTP header. They are best suited for server-to-server communication where a human user context is not required.
- Pros: Extremely easy to implement; simple for clients to use.
- Cons: Hard to rotate securely without downtime; broad access if compromised.
- Security Rule: Never store API keys in plain text in your database. Store a cryptographic hash (e.g., SHA-256) and compare the hashed incoming key.
middleware/apiKeyAuth.js// Express.js Server Validation Example
import crypto from 'crypto';
export const apiKeyAuth = async (req, res, next) => {
const apiKey = req.header('X-API-Key');
if (!apiKey) return res.status(401).json({ error: 'Missing API Key' });
// Hash the incoming key to compare with the database
const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
const keyRecord = await db.apiKeys.find({ hash: hashedKey });
if (!keyRecord) return res.status(403).json({ error: 'Invalid API Key' });
req.apiClient = keyRecord;
next();
};2. JSON Web Tokens (JWT) & The Refresh Lifecycle
JWTs are stateless, digitally signed tokens containing a JSON payload. Because they cannot be easily revoked once issued, you must implement a Dual-Token Architecture (Access + Refresh tokens).
- Access Token: Short-lived (e.g., 15 mins). Stored in memory on the frontend. Passed as a Bearer token.
- Refresh Token: Long-lived (e.g., 7 days). Stored in an
HttpOnly,Securecookie. Used solely to get new Access Tokens. - Security Rule: Never use symmetric signing algorithms (HS256) for public APIs. Use asymmetric algorithms (RS256) so microservices can verify the token using a public key without needing the private signing key.
controllers/authController.js// Setting a secure Refresh Token cookie during login
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true, // Prevents JavaScript access (mitigates XSS)
secure: process.env.NODE_ENV === 'production', // HTTPS only
sameSite: 'Strict', // Mitigates CSRF
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});3. OAuth 2.0 & OpenID Connect (OIDC)
OAuth 2.0 is an authorization framework allowing third-party applications to obtain limited access to an HTTP service. OIDC adds an identity layer on top.
- Pros: Industry standard for delegated access; robust security models.
- Security Rule: For Single Page Applications (SPAs) and Mobile Apps, always use the Authorization Code Flow with PKCE (Proof Key for Code Exchange). It prevents authorization code interception attacks.
client/pkce.js// Generating a PKCE Challenge on the Client
const codeVerifier = generateRandomString(64);
// Hash the verifier using SHA-256 and base64url encode it
const codeChallenge = base64URLEncode(sha256(codeVerifier));
// Redirect user to authorization server
const authUrl = `https://auth.example.com/authorize?
response_type=code&
client_id=${clientId}&
redirect_uri=${redirectUri}&
code_challenge=${codeChallenge}&
code_challenge_method=S256`;4. Token Revocation & Refresh Token Rotation
JWTs are stateless by design, which makes revocation difficult. In production systems, you must implement active session invalidation strategies to handle password changes, account compromise, and forced logout events.
- Refresh Token Rotation: Issue a new refresh token on every refresh request and immediately invalidate the old one.
- Replay Detection: If an old refresh token is reused, assume token theft and revoke the entire session.
- Blacklist Strategy: Store revoked JWT IDs (jti) in Redis for high-security environments.
- Short Access Token TTL: Keep access tokens valid for 5–15 minutes maximum.
services/tokenRevocation.js// Example: Blacklisting a revoked JWT using Redis
import Redis from 'ioredis';
const redis = new Redis();
export async function revokeToken(jti, expiresInSeconds) {
await redis.set(
`revoked:${jti}`,
'true',
'EX',
expiresInSeconds
);
}
export async function isTokenRevoked(jti) {
return await redis.get(`revoked:${jti}`);
}5. Mutual TLS (mTLS) for Internal & B2B APIs
Mutual TLS requires both client and server to present valid certificates. This is commonly used in financial systems, enterprise B2B integrations, and internal microservice communication.
- Strong Machine Identity: Authentication is bound to a certificate.
- No Shared Secrets: Eliminates API key distribution risks.
- Zero Trust Compatible: Every service must prove identity.
server/mtlsServer.js// Express server configured for mTLS
import https from 'https';
import fs from 'fs';
import app from './app.js';
const server = https.createServer({
key: fs.readFileSync('./certs/server-key.pem'),
cert: fs.readFileSync('./certs/server-cert.pem'),
ca: fs.readFileSync('./certs/ca-cert.pem'),
requestCert: true,
rejectUnauthorized: true
}, app);
server.listen(443, () => {
console.log('mTLS server running on port 443');
});6. Multi-Factor Authentication (MFA)
Passwords alone are insufficient for modern applications. MFA drastically reduces account takeover risk.
- TOTP: Time-based one-time passwords (Google Authenticator).
- WebAuthn / Passkeys: Phishing-resistant authentication.
- SMS OTP: Legacy fallback (less secure).
services/mfaService.js// TOTP verification example using speakeasy
import speakeasy from 'speakeasy';
export function verifyTOTP(secret, token) {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 1
});
}7. API Gateway & Centralized Token Verification
In microservice architectures, JWT verification should occur at the API Gateway. Downstream services trust validated identity headers.
gateway/authMiddleware.js// Example middleware at API Gateway level
export const gatewayAuth = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).send('Unauthorized');
const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY);
// Inject identity headers for internal services
req.headers['x-user-id'] = decoded.sub;
req.headers['x-user-scopes'] = decoded.scopes.join(',');
next();
};8. Secret Management & JWT Key Rotation
Signing keys must be rotated periodically. Use the JWT kidheader to support multiple active public keys during rotation.
auth/keyRotation.js// Signing JWT with a key ID (kid)
const token = jwt.sign(payload, privateKey, {
algorithm: 'RS256',
keyid: 'key-2026-01'
});
// Verifying using dynamic key resolution
function getPublicKey(header, callback) {
const key = keyStore[header.kid];
callback(null, key);
}
jwt.verify(token, getPublicKey);Crucial Implementation Checklist
- Strict HTTPS & HSTS: Enforce HTTP Strict Transport Security (HSTS). Credentials passed over HTTP are visible to anyone on the network.
- Zero Credentials in URLs: Never pass tokens or keys in query parameters (e.g.,
?token=xyz). URLs are logged in server access logs and browser history. - Rate Limiting by Identity: Do not just rate limit by IP address, as clients can cycle IPs. Rate limit authenticated routes based on the API Key or User ID to prevent noisy-neighbor attacks.