Every backend faces the same question: how do we authenticate requests?
Auth Model Comparison
| Feature | API Keys | JWTs | OAuth2 |
|---|---|---|---|
| Best for | Server-to-server | User sessions, SPAs | Delegated access |
| Stateless? | No | Yes | Depends |
| Revocable? | Instantly | Not until expiry | Yes |
| Complexity | Low | Medium | High |
1. API Key Authentication
Two rules: never store keys in plaintext, always use a prefix.
function generateApiKey() {
const prefix = "pk_live";
const secret = crypto.randomBytes(32).toString("base64url");
const fullKey = `${prefix}_${secret}`;
const hash = crypto.createHash("sha256").update(fullKey).digest("hex");
return { key: fullKey, record: { id: crypto.randomUUID(), prefix, hash, createdAt: new Date(), lastUsedAt: null } };
}
async function apiKeyAuth(req: Request, res: Response, next: NextFunction) {
const header = req.headers["x-api-key"];
if (!header) return res.status(401).json({ error: "Missing API key" });
const hash = crypto.createHash("sha256").update(header).digest("hex");
const record = await db.apiKeys.findOne({ hash });
if (!record) return res.status(401).json({ error: "Invalid API key" });
req.apiClient = record;
next();
}
2. JWT Authentication
Token Generation
function generateTokenPair(user: { sub: string; email: string; roles: string[] }) {
const accessToken = jwt.sign(
{ sub: user.sub, email: user.email, roles: user.roles },
ACCESS_SECRET,
{ expiresIn: "15m", issuer: "api.yourapp.com", audience: "yourapp.com" }
);
const refreshToken = jwt.sign(
{ sub: user.sub, type: "refresh" },
REFRESH_SECRET,
{ expiresIn: "7d", issuer: "api.yourapp.com", jwtid: crypto.randomUUID() }
);
return { accessToken, refreshToken };
}
Verification Middleware
function jwtAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) return res.status(401).json({ error: "Missing token" });
try {
req.user = jwt.verify(authHeader.slice(7), ACCESS_SECRET, {
issuer: "api.yourapp.com", audience: "yourapp.com", algorithms: ["HS256"]
});
next();
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}
}
Role-Based Access
function requireRoles(...roles: string[]) {
return (req, res, next) => {
if (!roles.some(r => req.user?.roles?.includes(r))) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
app.delete("/api/users/:id", jwtAuth, requireRoles("admin"), deleteUser);
3. OAuth2 Authorization Code Flow
Token Exchange
async function exchangeCode(code: string, config: OAuth2Config, codeVerifier?: string) {
const params = new URLSearchParams({
grant_type: "authorization_code", code,
redirect_uri: config.redirectUri, client_id: config.clientId,
});
if (config.clientSecret) params.set("client_secret", config.clientSecret);
if (codeVerifier) params.set("code_verifier", codeVerifier);
const res = await fetch(config.tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});
return res.json();
}
PKCE
function generatePKCE() {
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
return { codeVerifier, codeChallenge, method: "S256" };
}
4. Layered Auth Middleware
function authenticate(...strategies: AuthStrategy[]) {
const handlers = { apiKey: apiKeyAuth, jwt: jwtAuth, oauth2: oauth2Auth };
return async (req, res, next) => {
for (const s of strategies) {
try {
await new Promise((resolve, reject) => {
handlers[s](req, res, err => err ? reject(err) : resolve());
});
return next();
} catch {}
}
res.status(401).json({ error: "Auth required" });
};
}
app.get("/api/v1/data", authenticate("apiKey", "oauth2"), dataHandler);
app.get("/api/admin", authenticate("jwt"), requireRoles("admin"), adminHandler);
Security Checklist
- [ ] Rate-limit auth endpoints
- [ ] Hash API keys with SHA-256
- [ ] Rotate secrets on schedule
- [ ] HTTPS only
- [ ] Validate all JWT claims (iss, aud, exp, alg)
- [ ] Use PKCE everywhere
- [ ] Short access token TTLs (15min max)
- [ ] Secure cookies: HttpOnly, Secure, SameSite=Strict
- [ ] Lock down CORS
- [ ] Log auth events
API keys for machines. JWTs for users. OAuth2 for delegation. Layer them together.
Production Backend Patterns series
If this was useful, consider:
- Sponsoring on GitHub to support more open-source tools
- Buying me a coffee on Ko-fi
You Might Also Like
- Build a Custom API Gateway in Node.js: Routing, JWT Auth, and Rate Limits (2026)
- API Rate Limiting with Redis: Token Bucket, Sliding Window, and Per-Client Limits
- Environment Variables Done Right: From .env Files to Production Configs
Follow me for more production-ready backend content!
If this helped you, buy me a coffee on Ko-fi!
Top comments (0)