API Authentication Methods Explained — API Keys vs JWT vs OAuth 2.0
"How should I authenticate this API?" is one of the most common design questions developers face, and the three usual answers — API keys, JWT, and OAuth 2.0 — solve different problems. Picking the wrong one adds needless complexity or leaves security gaps. Here is how each works and when to reach for it.
API Keys
An API key is a static, opaque string issued to a client. The server looks it up in a database to identify who is calling and what they're allowed to do. It carries no information itself — it's just an identifier.
GET /api/weather?city=Mumbai HTTP/1.1
X-API-Key: YOUR_API_KEY_HERE
// Server-side check
const key = req.headers['x-api-key'];
const client = await db.apiKeys.findOne({ key, active: true });
if (!client) return res.status(401).json({ error: 'Invalid API key' });Best for server-to-server calls, public data APIs, and simple internal tools. Weak point: if a key leaks, it's valid until manually revoked, and keys typically don't expire on their own.
JWT Bearer Tokens
A JSON Web Token is a self-contained, signed token. It encodes claims (user ID, roles, expiry) directly in its payload, so the server can verify it using a signature check without a database round trip.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImV4cCI6MTcxOTg2MDAwMH0.4z1p...
// Node.js verification (jsonwebtoken)
const jwt = require('jsonwebtoken');
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}Best for stateless authentication in your own frontend/backend systems — login sessions, mobile app APIs. Weak point: a JWT cannot be easily revoked before it expires unless you maintain a blocklist, which reintroduces server-side state.
OAuth 2.0
OAuth 2.0 is not itself a token format — it's a protocol for granting a third-party application limited access to a user's resources without ever sharing the user's password. The most common flow is Authorization Code:
- User clicks "Sign in with Google" on your app
- Your app redirects to Google's authorization server
- User logs in and approves the requested scopes (e.g. "read your email")
- Google redirects back to your app with a one-time authorization code
- Your backend exchanges that code for an access token (and optionally a refresh token)
- Your app uses the access token to call Google's API on the user's behalf
Best for third-party integrations, "Sign in with X" flows, and delegated access. It's deliberately more complex than API keys or plain JWTs because it's solving a harder problem: granting scoped access without ever exposing credentials.
Side-by-Side Comparison
- API keys — simplest, no expiry by default, best for server-to-server and public APIs
- JWT — stateless, self-verifying, best for your own app's login sessions, harder to revoke early
- OAuth 2.0 — most complex, purpose-built for delegated third-party access without sharing passwords
- Many real systems combine them — OAuth 2.0 to authenticate a user, then issue a JWT as the app's own session token
Frequently Asked Questions
An API key is an opaque random string looked up in a database to identify the caller, with no information encoded in it. A JWT is a self-contained token that encodes claims like user ID and expiry directly in its payload, verifiable with a signature, without a database lookup.
Use OAuth 2.0 when a third-party application needs limited access to a user's account without ever seeing their password, such as "Sign in with Google" or letting an app post to a user's social media on their behalf. Use API keys for simple server-to-server or internal integrations.
Storing JWTs in localStorage exposes them to theft via XSS attacks, since any injected script can read localStorage. A more secure pattern is storing the token in an httpOnly, Secure cookie, which JavaScript cannot access.