API Key Management Best Practices

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

TRY NANO BANANA FOR FREE

API Key Management Best Practices

TRY NANO BANANA FOR FREE
Contents

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:

  1. Generation: Use prefixes and strong randomness
  2. Storage: Hash keys, never store plain text
  3. Validation: Check hash, expiration, and status
  4. Rotation: Automatic expiration and manual rotation
  5. Revocation: Immediate and bulk revocation
  6. 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