Building an API Key Authentication System
Why Build API Key Authentication?
API keys are the simplest and most widely used authentication method for server-to-server communication. They are used by Stripe, Google Cloud, OpenAI, Twilio, and nearly every major API platform. Building your own API key system teaches you core security patterns that apply to all authentication methods.
Why this matters for your career:
- API key systems are a common freelance project requirement
- Understanding key generation, storage, and validation is essential for backend security
- Many SaaS products need a developer portal with API key management
- You will use API keys daily as a developer — knowing how they work internally is invaluable
What We Are Building
A complete API key authentication system with:
- Secure key generation
- Hashed key storage
- Request validation middleware
- Key rotation and revocation
- Rate limiting per key
- Usage tracking
- Developer dashboard
Step 1: Secure Key Generation
API keys must be cryptographically random and unpredictable:
const crypto = require('crypto');
function generateApiKey() {
// Generate 32 random bytes and encode as hex
const rawKey = crypto.randomBytes(32).toString('hex');
// Prefix with a recognizable string for easy identification
return `vibe_${rawKey}`;
// Example output: vibe_a1b2c3d4e5f6... (64 characters)
}
For production systems, use a prefix to identify the key type:
| Prefix | Purpose | Example |
|--------|---------|--------|
| sk_live_ | Production secret key | sk_live_a1b2c3d4... |
| sk_test_ | Test/development key | sk_test_e5f6g7h8... |
| pk_live_ | Publishable key (client-side) | pk_live_i9j0k1l2... |
| whsec_ | Webhook signing secret | whsec_m3n4o5p6... |
Step 2: Store Keys Securely
Never store API keys in plaintext. Hash them before saving to the database, just like passwords:
const crypto = require('crypto');
function hashApiKey(apiKey) {
return crypto.createHash('sha256').update(apiKey).digest('hex');
}
// Store in database
const keyHash = hashApiKey(rawApiKey);
await db.query(
'INSERT INTO api_keys (user_id, key_hash, prefix, name, created_at) VALUES ($1, $2, $3, $4, $5)',
[userId, keyHash, 'vibe', 'Production API Key', new Date()]
);
// Return the raw key ONLY once — display to user immediately
return { apiKey: rawApiKey, keyId: result.insertId };
Step 3: Validate API Keys on Every Request
Create middleware that intercepts every API request and validates the key:
async function apiKeyMiddleware(req, res, next) {
// Extract API key from header
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({
error: {
code: 'MISSING_API_KEY',
message: 'Please provide an API key in the X-API-Key header.'
}
});
}
// Hash the provided key and look it up
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
const keyRecord = await db.query(
'SELECT * FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL',
[keyHash]
);
if (!keyRecord) {
return res.status(401).json({
error: {
code: 'INVALID_API_KEY',
message: 'The provided API key is invalid or has been revoked.'
}
});
}
// Attach key info to request for downstream use
req.apiKey = {
keyId: keyRecord.id,
userId: keyRecord.user_id,
name: keyRecord.name
};
// Track usage
await db.query(
'INSERT INTO api_key_usage (key_id, endpoint, ip_address, created_at) VALUES ($1, $2, $3, $4)',
[keyRecord.id, req.path, req.ip, new Date()]
);
next();
}
// Apply to all API routes
app.use('/api/v1', apiKeyMiddleware);
Step 4: Rate Limiting Per API Key
Different keys have different rate limits. Implement per-key rate limiting:
const rateLimit = require('express-rate-limit');
// Dynamic rate limiter based on key's tier
function keyRateLimiter(req, res, next) {
const tierLimits = {
free: { windowMs: 3600000, max: 100 }, // 100 req/hour
pro: { windowMs: 3600000, max: 10000 }, // 10,000 req/hour
enterprise: { windowMs: 3600000, max: 100000 } // 100,000 req/hour
};
const limit = tierLimits[req.apiKey.tier] || tierLimits.free;
const limiter = rateLimit({
windowMs: limit.windowMs,
max: limit.max,
keyGenerator: (req) => req.apiKey.keyId.toString(),
message: {
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: `Rate limit exceeded. Your tier allows ${limit.max} requests per hour.`
}
}
});
return limiter(req, res, next);
}
app.use('/api/v1', apiKeyMiddleware, keyRateLimiter);
Step 5: Key Rotation and Revocation
Users should be able to revoke and rotate keys from a dashboard:
async function revokeApiKey(keyId, userId) {
await db.query(
'UPDATE api_keys SET revoked_at = $1 WHERE id = $2 AND user_id = $3',
[new Date(), keyId, userId]
);
return { message: 'API key revoked successfully.' };
}
async function rotateApiKey(oldKeyId, userId) {
// Revoke old key
await revokeApiKey(oldKeyId, userId);
// Generate new key
const rawKey = generateApiKey();
const keyHash = hashApiKey(rawKey);
await db.query(
'INSERT INTO api_keys (user_id, key_hash, prefix, name, created_at) VALUES ($1, $2, $3, $4, $5)',
[userId, keyHash, 'vibe', 'Rotated Key', new Date()]
);
return { apiKey: rawKey, message: 'New API key generated. Old key revoked.' };
}
Step 6: Developer Dashboard
Build a simple dashboard where developers can manage their keys:
// GET /dashboard/api-keys — list all keys for the current user
app.get('/dashboard/api-keys', async (req, res) => {
const keys = await db.query(
`SELECT id, prefix, name, created_at,
(SELECT COUNT(*) FROM api_key_usage WHERE key_id = api_keys.id) as usage_count
FROM api_keys
WHERE user_id = $1 AND revoked_at IS NULL
ORDER BY created_at DESC`,
[req.session.userId]
);
res.json(keys);
});
// POST /dashboard/api-keys — create a new key
app.post('/dashboard/api-keys', async (req, res) => {
const rawKey = generateApiKey();
const keyHash = hashApiKey(rawKey);
await db.query(
'INSERT INTO api_keys (user_id, key_hash, prefix, name) VALUES ($1, $2, $3, $4)',
[req.session.userId, keyHash, 'vibe', req.body.name || 'My API Key']
);
res.json({ apiKey: rawKey, message: 'Copy this key now. It will not be shown again.' });
});
// DELETE /dashboard/api-keys/:id — revoke a key
app.delete('/dashboard/api-keys/:id', async (req, res) => {
await revokeApiKey(req.params.id, req.session.userId);
res.json({ message: 'API key revoked.' });
});
Database Schema
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
key_hash VARCHAR(64) NOT NULL,
prefix VARCHAR(16) NOT NULL,
name VARCHAR(255) NOT NULL DEFAULT 'My API Key',
tier VARCHAR(20) NOT NULL DEFAULT 'free',
created_at TIMESTAMP DEFAULT NOW(),
revoked_at TIMESTAMP DEFAULT NULL,
last_used_at TIMESTAMP DEFAULT NULL
);
CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash);
CREATE INDEX idx_api_keys_user_id ON api_keys(user_id);
CREATE TABLE api_key_usage (
id BIGSERIAL PRIMARY KEY,
key_id UUID NOT NULL REFERENCES api_keys(id),
endpoint VARCHAR(255) NOT NULL,
status_code INT NOT NULL,
ip_address VARCHAR(45),
response_time_ms INT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_api_key_usage_key_id ON api_key_usage(key_id);
CREATE INDEX idx_api_key_usage_created_at ON api_key_usage(created_at);
Security Checklist
| Requirement | Implemented | |-------------|-------------| | Keys are cryptographically random | ✅ crypto.randomBytes(32) | | Keys are hashed before storage | ✅ SHA-256 | | Raw key shown only once on creation | ✅ Display immediately, never store | | Multiple keys per user | ✅ Users can create/revoke multiple keys | | Key revocation | ✅ revoked_at timestamp | | Per-key rate limiting | ✅ Tier-based limits | | Usage tracking | ✅ api_key_usage table | | Audit log | ✅ Track endpoint, IP, timestamp | | No keys in client-side code | ✅ Documentation warns against this | | Key rotation support | ✅ rotateApiKey() function |
Summary
Building an API key authentication system teaches you fundamental security patterns: cryptographic key generation, hashed storage, middleware-based validation, per-key rate limiting, and developer dashboard design. These patterns apply to any API authentication system you build.
Key takeaways:
- Generate keys using cryptographically secure random bytes
- Hash keys with SHA-256 before storing in the database
- Show the raw key to the user exactly once on creation
- Validate keys in middleware before every protected request
- Implement per-key rate limiting based on pricing tier
- Support key revocation and rotation from a developer dashboard
- Track usage for analytics, billing, and abuse detection
- Never accept keys in query parameters or store them in client-side code
What's Next: Next.js App Router Course
The next course covers the Next.js App Router — building modern full-stack React applications with server components, API routes, and middleware.