Meta Description: Compare API keys, OAuth 2.0, and JWT for API authentication. Learn the strengths, weaknesses, and ideal use cases for each method.
Keywords: api authentication, api keys, oauth 2.0, jwt tokens, authentication methods, api security
Word Count: ~2,400 words
You need to authenticate API requests. Should you use API keys, OAuth, or JWT?
Each method has different security properties, complexity, and use cases. The right choice depends on your API's requirements.
Let's compare them.
Quick Comparison
| Feature | API Keys | OAuth 2.0 | JWT |
|---|---|---|---|
| Complexity | Low | High | Medium |
| Security | Basic | High | Medium-High |
| Expiration | Manual | Automatic | Automatic |
| Revocation | Easy | Easy | Hard |
| User Context | No | Yes | Yes |
| Scopes | No | Yes | Yes |
| Best For | Server-to-server | User authorization | Stateless auth |
API Keys: Simple and Direct
API keys are long random strings that identify the caller.
How API Keys Work
- Generate a key:
sk_live_abc123def456... - Client includes it in requests:
GET /v1/pets Authorization: Bearer sk_live_abc123def456... - Server validates the key against the database
Generating API Keys
const crypto = require('crypto');
function generateApiKey() {
const prefix = 'sk_live_'; // or sk_test_ for test keys
const randomBytes = crypto.randomBytes(32).toString('hex');
return prefix + randomBytes;
}
// Store in database
async function createApiKey(userId) {
const key = generateApiKey();
const hashedKey = crypto
.createHash('sha256')
.update(key)
.digest('hex');
await db.apiKeys.create({
userId: userId,
keyHash: hashedKey,
prefix: key.substring(0, 12), // For display
createdAt: new Date()
});
// Return the key once (never stored in plain text)
return key;
}
Validating API Keys
async function validateApiKey(key) {
const hashedKey = crypto
.createHash('sha256')
.update(key)
.digest('hex');
const apiKey = await db.apiKeys.findOne({
keyHash: hashedKey,
status: 'active'
});
if (!apiKey) {
return null;
}
// Update last used timestamp
await db.apiKeys.update(apiKey.id, {
lastUsedAt: new Date()
});
return {
userId: apiKey.userId,
scopes: apiKey.scopes || []
};
}
// Middleware
app.use(async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing API key' });
}
const apiKey = authHeader.substring(7);
const auth = await validateApiKey(apiKey);
if (!auth) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.auth = auth;
next();
});
API Key Strengths
1. Simple to implement
No OAuth flows, no token refresh, no complex protocols. Generate a key, validate it.
2. Easy to understand
Developers understand API keys immediately. No learning curve.
3. Long-lived
Keys don't expire automatically. Good for server-to-server communication.
4. Easy to revoke
Delete the key from the database. Instant revocation.
API Key Weaknesses
1. No automatic expiration
Keys live forever unless manually revoked. Compromised keys remain valid.
2. No user context
Keys identify the application, not the user. You can't know which user made the request.
3. No fine-grained permissions
Keys typically have full access. Hard to implement scopes.
4. Rotation is manual
No automatic key rotation. Developers must rotate keys manually.
When to Use API Keys
Server-to-server APIs: Backend services calling your API Internal APIs: Microservices within your infrastructure Simple integrations: Webhooks, cron jobs, scripts Developer tools: CLI tools, SDKs
Don't use API keys for: - User-facing applications - Mobile apps (keys can be extracted) - Browser apps (keys exposed in source)
OAuth 2.0: User Authorization
OAuth 2.0 lets users authorize third-party apps without sharing passwords.
How OAuth 2.0 Works
- User clicks "Connect with PetStore"
- Redirected to authorization page
- User grants permissions (scopes)
- App receives authorization code
- App exchanges code for access token
- App uses token to call API
Authorization Code Flow
// Step 1: Redirect to authorization page
app.get('/auth/petstore', (req, res) => {
const authUrl = new URL('https://api.petstoreapi.com/oauth/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'read:pets write:pets');
authUrl.searchParams.set('state', generateState()); // CSRF protection
res.redirect(authUrl.toString());
});
// Step 2: Handle callback
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state (CSRF protection)
if (!verifyState(state)) {
return res.status(400).json({ error: 'Invalid state' });
}
// Exchange code for token
const tokenResponse = await fetch('https://api.petstoreapi.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: 'https://myapp.com/callback'
})
});
const tokens = await tokenResponse.json();
// tokens = { access_token, refresh_token, expires_in, scope }
// Store tokens securely
await storeTokens(req.session.userId, tokens);
res.redirect('/dashboard');
});
// Step 3: Use access token
app.get('/my-pets', async (req, res) => {
const tokens = await getTokens(req.session.userId);
const response = await fetch('https://api.petstoreapi.com/v1/pets', {
headers: {
'Authorization': `Bearer ${tokens.access_token}`
}
});
const pets = await response.json();
res.json(pets);
});
Token Refresh
Access tokens expire (typically 1 hour). Use refresh tokens to get new access tokens:
async function refreshAccessToken(refreshToken) {
const response = await fetch('https://api.petstoreapi.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
})
});
return await response.json();
}
// Automatic refresh
async function callApiWithRefresh(url, tokens) {
let response = await fetch(url, {
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
});
if (response.status === 401) {
// Token expired, refresh it
const newTokens = await refreshAccessToken(tokens.refresh_token);
await storeTokens(userId, newTokens);
// Retry request
response = await fetch(url, {
headers: { 'Authorization': `Bearer ${newTokens.access_token}` }
});
}
return response;
}
OAuth 2.0 Strengths
1. User authorization
Users grant specific permissions. Apps act on behalf of users.
2. No password sharing
Users never share passwords with third-party apps.
3. Fine-grained scopes
Users grant only the permissions apps need.
4. Automatic expiration
Access tokens expire automatically. Limits damage from leaks.
5. Revocable
Users can revoke access anytime.
OAuth 2.0 Weaknesses
1. Complex
Multiple flows, token refresh, state management. High implementation complexity.
2. Requires user interaction
Not suitable for server-to-server communication.
3. Token storage
Apps must securely store refresh tokens.
When to Use OAuth 2.0
Third-party integrations: Apps accessing user data User-facing APIs: Mobile apps, web apps Delegated access: Apps acting on behalf of users
JWT: Stateless Tokens
JWT (JSON Web Tokens) are self-contained tokens that include claims about the user.
How JWT Works
- User logs in with credentials
- Server generates JWT with user info
- Client includes JWT in requests
- Server validates JWT signature (no database lookup)
JWT Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three parts (separated by .): 1. Header: Algorithm and token type 2. Payload: Claims (user ID, scopes, expiration) 3. Signature: Verifies integrity
Generating JWTs
const jwt = require('jsonwebtoken');
function generateJWT(user) {
const payload = {
sub: user.id, // Subject (user ID)
email: user.email,
scopes: user.scopes,
iat: Math.floor(Date.now() / 1000), // Issued at
exp: Math.floor(Date.now() / 1000) + 3600 // Expires in 1 hour
};
return jwt.sign(payload, process.env.JWT_SECRET, {
algorithm: 'HS256'
});
}
// Login endpoint
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateJWT(user);
res.json({ token });
});
Validating JWTs
function validateJWT(token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return decoded;
} catch (error) {
return null;
}
}
// Middleware
app.use((req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.substring(7);
const decoded = validateJWT(token);
if (!decoded) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = decoded;
next();
});
JWT Strengths
1. Stateless
No database lookup needed. Server validates signature only.
2. Scalable
Any server can validate tokens. No shared session storage.
3. Self-contained
Token includes all needed information (user ID, scopes).
4. Cross-domain
Works across different domains and services.
JWT Weaknesses
1. Hard to revoke
Once issued, tokens are valid until expiration. Can't revoke immediately.
2. Token size
JWTs are larger than API keys (200-500 bytes vs 32 bytes).
3. Secret management
Must protect the signing secret. If leaked, attackers can forge tokens.
4. No refresh mechanism
JWT spec doesn't define refresh tokens. Must implement separately.
When to Use JWT
Microservices: Stateless auth across services Single-page apps: Browser-based applications Mobile apps: Native mobile applications Short-lived sessions: When immediate revocation isn't critical
Combining Methods
Many APIs use multiple methods:
Stripe: - API keys for server-to-server - OAuth for third-party integrations
GitHub: - Personal access tokens (like API keys) - OAuth for apps - JWT for GitHub Apps
Modern PetStore API: - API keys for backend services - OAuth for third-party apps - JWT for mobile/web apps
Decision Matrix
Use API Keys when: - Server-to-server communication - Internal services - Simple integrations - Long-lived access needed
Use OAuth 2.0 when: - Third-party apps need user data - User authorization required - Fine-grained permissions needed - Revocation is important
Use JWT when: - Stateless auth required - Microservices architecture - Cross-domain authentication - Scalability is critical
Security Best Practices
For all methods: - Use HTTPS only - Implement rate limiting - Log authentication attempts - Monitor for suspicious activity
For API keys: - Hash keys before storing - Use prefixes (sk_live_, sk_test_) - Rotate keys regularly - Never commit keys to git
For OAuth: - Validate redirect URIs - Use state parameter (CSRF protection) - Store refresh tokens securely - Implement token rotation
For JWT: - Use strong secrets (256+ bits) - Set short expiration times - Validate all claims - Consider token blacklisting for revocation
Conclusion
There's no one-size-fits-all authentication method. Choose based on your use case:
- Simple server-to-server: API keys
- User authorization: OAuth 2.0
- Stateless microservices: JWT
- Complex systems: Combine multiple methods
The Modern PetStore API supports all three, letting clients choose what works best for them.
Related Articles: - OAuth 2.0 Scopes: Fine-Grained API Permissions - JWT Best Practices: Security and Performance - API Security Checklist: 20 Essential Practices