Meta Description: Manage API keys securely with proper generation, storage, rotation, and revocation. Learn best practices for key prefixes, hashing, and lifecycle management.
Keywords: api key management, api key security, key rotation, api key generation, secure api keys, key revocation
Word Count: ~2,200 words
Your API uses API keys for authentication. But how do you manage them securely?
Poor key management leads to: - Keys leaked in code repositories - Compromised keys that can't be revoked - No visibility into key usage - Keys that never expire
Here's how to manage API keys properly.
Generating Secure API Keys
Key Format
Use a consistent format with prefixes:
sk_live_abc123def456...
│ │ │
│ │ └─ Random bytes (32+ characters)
│ └────── Environment (live, test)
└───────── Prefix (identifies key type)
Benefits: - Identifiable: You know it's an API key - Scannable: Tools can detect leaked keys - Typed: Different prefixes for different key types
Generation Code
const crypto = require('crypto');
function generateApiKey(type = 'live') {
const prefix = `sk_${type}_`;
const randomBytes = crypto.randomBytes(32).toString('hex');
return prefix + randomBytes;
}
// Examples
const liveKey = generateApiKey('live');
// sk_live_a1b2c3d4e5f6...
const testKey = generateApiKey('test');
// sk_test_x9y8z7w6v5u4...
Key Types
Use different prefixes for different purposes:
const KEY_TYPES = {
SECRET: 'sk', // Secret key (server-side)
PUBLIC: 'pk', // Public key (client-side)
RESTRICTED: 'rk', // Restricted key (limited permissions)
WEBHOOK: 'whsec' // Webhook signing secret
};
function generateApiKey(type, environment = 'live') {
const prefix = `${KEY_TYPES[type]}_${environment}_`;
const randomBytes = crypto.randomBytes(32).toString('hex');
return prefix + randomBytes;
}
Storing API Keys Securely
Never Store Plain Text
Hash keys before storing:
async function createApiKey(userId, name) {
const key = generateApiKey('live');
// Hash the key
const hashedKey = crypto
.createHash('sha256')
.update(key)
.digest('hex');
// Store only the hash
await db.apiKeys.create({
id: generateId(),
userId: userId,
name: name,
keyHash: hashedKey,
prefix: key.substring(0, 15), // For display: "sk_live_abc123..."
createdAt: new Date(),
lastUsedAt: null,
status: 'active'
});
// Return the key ONCE (never stored in plain text)
return key;
}
Database Schema
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
name VARCHAR(255) NOT NULL,
key_hash VARCHAR(64) NOT NULL UNIQUE,
prefix VARCHAR(20) NOT NULL,
scopes TEXT[] DEFAULT '{}',
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMP,
expires_at TIMESTAMP,
revoked_at TIMESTAMP,
revoked_reason TEXT,
INDEX idx_user_id (user_id),
INDEX idx_key_hash (key_hash),
INDEX idx_status (status)
);
Validating API Keys
Validation Function
async function validateApiKey(key) {
// Hash the provided key
const hashedKey = crypto
.createHash('sha256')
.update(key)
.digest('hex');
// Look up by hash
const apiKey = await db.apiKeys.findOne({
keyHash: hashedKey,
status: 'active'
});
if (!apiKey) {
return null;
}
// Check expiration
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
return null;
}
// Update last used timestamp
await db.apiKeys.update(apiKey.id, {
lastUsedAt: new Date()
});
return {
userId: apiKey.userId,
scopes: apiKey.scopes || [],
keyId: apiKey.id
};
}
Middleware
async function authenticateApiKey(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Missing API key',
message: 'Provide an API key in the Authorization header'
});
}
const apiKey = authHeader.substring(7);
const auth = await validateApiKey(apiKey);
if (!auth) {
return res.status(401).json({
error: 'Invalid API key',
message: 'The provided API key is invalid or has been revoked'
});
}
req.auth = auth;
next();
}
app.use('/v1/', authenticateApiKey);
Key Rotation
Automatic Expiration
Set expiration dates on keys:
async function createApiKey(userId, name, expiresInDays = 90) {
const key = generateApiKey('live');
const hashedKey = crypto.createHash('sha256').update(key).digest('hex');
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + expiresInDays);
await db.apiKeys.create({
id: generateId(),
userId: userId,
name: name,
keyHash: hashedKey,
prefix: key.substring(0, 15),
expiresAt: expiresAt,
status: 'active'
});
return { key, expiresAt };
}
Rotation Warnings
Warn users before keys expire:
async function checkExpiringKeys() {
const thirtyDaysFromNow = new Date();
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
const expiringKeys = await db.apiKeys.findMany({
status: 'active',
expiresAt: {
$lte: thirtyDaysFromNow,
$gte: new Date()
}
});
for (const key of expiringKeys) {
await sendEmail(key.userId, {
subject: 'API Key Expiring Soon',
body: `Your API key "${key.name}" (${key.prefix}...) expires on ${key.expiresAt}. Please rotate it.`
});
}
}
// Run daily
setInterval(checkExpiringKeys, 24 * 60 * 60 * 1000);
Manual Rotation
Let users rotate keys:
app.post('/v1/api-keys/:id/rotate', async (req, res) => {
const oldKey = await db.apiKeys.findOne({
id: req.params.id,
userId: req.auth.userId
});
if (!oldKey) {
return res.status(404).json({ error: 'Key not found' });
}
// Generate new key
const newKey = generateApiKey('live');
const hashedKey = crypto.createHash('sha256').update(newKey).digest('hex');
// Create new key
const newApiKey = await db.apiKeys.create({
id: generateId(),
userId: req.auth.userId,
name: oldKey.name,
keyHash: hashedKey,
prefix: newKey.substring(0, 15),
scopes: oldKey.scopes,
status: 'active'
});
// Mark old key as rotated (keep for grace period)
await db.apiKeys.update(oldKey.id, {
status: 'rotated',
rotatedTo: newApiKey.id,
rotatedAt: new Date()
});
res.json({
key: newKey,
message: 'Key rotated successfully. Old key will work for 7 days.'
});
});
Key Revocation
Immediate Revocation
app.delete('/v1/api-keys/:id', async (req, res) => {
const apiKey = await db.apiKeys.findOne({
id: req.params.id,
userId: req.auth.userId
});
if (!apiKey) {
return res.status(404).json({ error: 'Key not found' });
}
await db.apiKeys.update(apiKey.id, {
status: 'revoked',
revokedAt: new Date(),
revokedReason: req.body.reason || 'User revoked'
});
res.status(204).send();
});
Bulk Revocation
Revoke all keys for a user:
app.post('/v1/api-keys/revoke-all', async (req, res) => {
await db.apiKeys.updateMany(
{ userId: req.auth.userId, status: 'active' },
{
status: 'revoked',
revokedAt: new Date(),
revokedReason: 'Bulk revocation by user'
}
);
res.json({ message: 'All API keys revoked' });
});
Compromise Detection
Automatically revoke compromised keys:
async function detectCompromisedKey(keyId) {
const key = await db.apiKeys.findById(keyId);
// Check for suspicious activity
const recentRequests = await db.apiLogs.findMany({
keyId: keyId,
timestamp: { $gte: new Date(Date.now() - 3600000) } // Last hour
});
const suspiciousPatterns = [
// Too many requests
recentRequests.length > 10000,
// Requests from multiple IPs
new Set(recentRequests.map(r => r.ip)).size > 10,
// Requests from unusual locations
recentRequests.some(r => isUnusualLocation(r.country))
];
if (suspiciousPatterns.some(p => p)) {
// Auto-revoke
await db.apiKeys.update(keyId, {
status: 'revoked',
revokedAt: new Date(),
revokedReason: 'Suspicious activity detected'
});
// Notify user
await sendEmail(key.userId, {
subject: 'API Key Revoked - Suspicious Activity',
body: 'Your API key was automatically revoked due to suspicious activity.'
});
}
}
Key Usage Tracking
Log All Requests
async function logApiRequest(req, keyId) {
await db.apiLogs.create({
keyId: keyId,
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.headers['user-agent'],
statusCode: res.statusCode,
timestamp: new Date()
});
}
Usage Dashboard
app.get('/v1/api-keys/:id/usage', async (req, res) => {
const key = await db.apiKeys.findOne({
id: req.params.id,
userId: req.auth.userId
});
if (!key) {
return res.status(404).json({ error: 'Key not found' });
}
const usage = await db.apiLogs.aggregate([
{ $match: { keyId: key.id } },
{
$group: {
_id: {
date: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } }
},
count: { $sum: 1 }
}
},
{ $sort: { '_id.date': -1 } },
{ $limit: 30 }
]);
res.json({
key: {
id: key.id,
name: key.name,
prefix: key.prefix,
createdAt: key.createdAt,
lastUsedAt: key.lastUsedAt
},
usage: usage
});
});
Best Practices
1. Use Key Prefixes
Makes keys identifiable and scannable:
sk_live_... ← Secret key, live environment
pk_test_... ← Public key, test environment
2. Hash Keys in Database
Never store plain text keys. Use SHA-256 or better.
3. Set Expiration Dates
Keys should expire after 90 days. Force rotation.
4. Implement Scopes
Limit what each key can do:
{
scopes: ['read:pets', 'write:pets']
}
5. Track Usage
Log every request. Detect anomalies.
6. Support Multiple Keys
Let users create multiple keys for different purposes.
7. Provide Test Keys
Separate test and live keys. Test keys don't affect production.
8. Show Last Used
Display when each key was last used. Helps identify unused keys.
9. Allow Naming
Let users name keys: "Production Server", "CI/CD Pipeline", etc.
10. Graceful Rotation
Keep old keys working for 7 days after rotation. Prevents downtime.
Summary
Secure API key management requires:
- Generation: Use prefixes and strong randomness
- Storage: Hash keys, never store plain text
- Validation: Check hash, expiration, and status
- Rotation: Automatic expiration and manual rotation
- Revocation: Immediate and bulk revocation
- Tracking: Log usage and detect anomalies
Implement these practices to keep your API keys secure.
Related Articles: - OAuth 2.0 Scopes: Fine-Grained API Permissions - API Keys vs OAuth vs JWT: Choosing the Right Authentication - API Security Best Practices: A Complete Checklist