Why API Authentication Matters
An unauthenticated API is an open door — anyone can read your data, exhaust your rate limits, or perform actions on behalf of your users. Authentication tells your API who is making a request. Authorization tells it what they're allowed to do.
The three dominant approaches to API authentication are API keys, JSON Web Tokens (JWT), and OAuth 2.0. Each solves a different problem.
API Keys
The simplest method. A long random string that identifies the caller.
Authorization: Bearer sk_live_4bD92jKm9xZqRt7LvPwN3cYa
# or as a query param (less secure):
GET /api/data?api_key=sk_live_4bD92jKm9xZqRt7LvPwN3cYa
How it works:
- User generates an API key in your dashboard
- Client sends the key with every request
- Server looks up the key in the database, finds the associated account, grants access
Implementation (Node.js/Express):
async function apiKeyMiddleware(req, res, next) {
const key = req.headers.authorization?.replace("Bearer ", "");
if (!key) return res.status(401).json({ error: "Missing API key" });
const account = await db.apiKeys.findOne({ key, active: true });
if (!account) return res.status(401).json({ error: "Invalid API key" });
req.account = account;
next();
}
Generating a secure key:
import crypto from "crypto";
const apiKey = "sk_live_" + crypto.randomBytes(32).toString("hex");
Pros: Simple to implement and use, stateless lookup, easy to revoke Cons: If leaked, full access until manually revoked. No expiry. Not suitable for user-facing apps.
Best for: Server-to-server integrations, developer tools, internal services, third-party API access (Stripe, Twilio, Unsplash all use this).
JWT (JSON Web Tokens)
A JWT is a self-contained token that carries claims about the user. The server doesn't need a database lookup to verify it — it just checks the signature.
JWT structure (three base64url-encoded parts separated by dots):
header.payload.signature
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMiLCJleHAiOjE3MTk4MDAwMDB9.abc123
Header: Algorithm used (HS256, RS256)
Payload: Claims — user data, expiry, roles
Signature: HMAC of header + payload, signed with your secret key
Generating a JWT (Node.js with jsonwebtoken):
import jwt from "jsonwebtoken";
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: "admin",
},
process.env.JWT_SECRET,
{ expiresIn: "15m" } // short-lived
);
Verifying a JWT:
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return res.status(401).json({ error: "Missing token" });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
next();
} catch {
return res.status(401).json({ error: "Invalid or expired token" });
}
}
Access + Refresh Token Pattern: Short-lived access tokens (15 min) + long-lived refresh tokens (7 days) stored in httpOnly cookies:
// Access token — short lived, sent in Authorization header
const accessToken = jwt.sign({ userId }, JWT_SECRET, { expiresIn: "15m" });
// Refresh token — long lived, stored in httpOnly cookie
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: "7d" });
res.cookie("refreshToken", refreshToken, {
httpOnly: true, // not accessible by JS — prevents XSS theft
secure: true, // HTTPS only
sameSite: "strict",
});
Pros: No database lookup per request (stateless), carries user data in the token, works across microservices Cons: Can't be revoked before expiry (without a blocklist), payload is base64-encoded (not encrypted — don't put secrets in it)
Best for: User authentication in SPAs and mobile apps, microservice-to-microservice auth, stateless sessions.
OAuth 2.0
OAuth 2.0 is an authorization framework that lets your app access resources on behalf of a user at a third-party service — without ever seeing their password.
When you click "Sign in with Google," that's OAuth 2.0.
The Authorization Code Flow (most secure):
- User clicks "Login with GitHub"
- You redirect to GitHub's authorization page
- User approves access
- GitHub redirects back to your app with an authorization code
- Your server exchanges the code for an access token (server-to-server, code never exposed in browser)
- Use the access token to call GitHub's API
// Step 2: Redirect to GitHub
const githubAuthUrl = new URL("https://github.com/login/oauth/authorize");
githubAuthUrl.searchParams.set("client_id", process.env.GITHUB_CLIENT_ID);
githubAuthUrl.searchParams.set("redirect_uri", "https://yourapp.com/auth/callback");
githubAuthUrl.searchParams.set("scope", "user:email");
githubAuthUrl.searchParams.set("state", generateRandomState()); // CSRF protection
res.redirect(githubAuthUrl.toString());
// Step 5: Exchange code for token
app.get("/auth/callback", async (req, res) => {
const { code, state } = req.query;
// verify state matches what you sent (CSRF check)
const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code,
}),
});
const { access_token } = await tokenResponse.json();
// Step 6: Use token to get user info
const userResponse = await fetch("https://api.github.com/user", {
headers: { Authorization: `Bearer ${access_token}` },
});
const githubUser = await userResponse.json();
// Create or log in your user based on githubUser.id
});
Pros: Users never share passwords with your app, delegates trust to a provider, industry standard Cons: Complex flow, dependent on third-party availability
Best for: "Login with Google/GitHub/Facebook," accessing third-party APIs on behalf of users.
Comparison Table
| API Keys | JWT | OAuth 2.0 | |
|---|---|---|---|
| Complexity | Low | Medium | High |
| Revocable | Yes (manual) | Only with blocklist | Yes |
| Stateless | No (DB lookup) | Yes | No (token exchange) |
| User identity | No | Yes | Yes |
| Third-party access | No | No | Yes |
| Best for | M2M, developer APIs | User auth, SPAs | Login with X |
Security Rules for All Methods
- Always use HTTPS — tokens in plaintext are stolen instantly on plain HTTP
- Never log tokens or API keys — keep them out of your logs
- Use short expiry times — 15 minutes for access tokens
- Store tokens in httpOnly cookies on the web (not localStorage — XSS can steal it)
- Rotate secrets if they're exposed
- Validate and sanitize everything in the payload
Conclusion
Use API keys for server-to-server and developer integrations. Use JWT for user authentication in your own app. Use OAuth 2.0 when you need to log users in with a third-party provider or access their data on external services. Each method has its place — the worst choice is mixing them without understanding the security tradeoffs.