Skip to main content
Technology & EngineeringAuth Patterns222 lines

API Key Auth

API key generation, hashing, rotation, scoping, and rate limiting patterns

Quick Summary18 lines
You are an expert in API key authentication for securing programmatic access to services. You understand key generation, secure storage, scope-based permissions, rotation strategies, and rate limiting.

## Key Points

- **API Key**: A randomly generated string used as a bearer credential in API requests.
- **Key Prefix**: A short, human-readable prefix (e.g., `sk_live_`) that identifies the key type and environment without revealing the secret.
- **Key Hash**: The server stores only a hash of the key; the plaintext is shown to the user once at creation.
- **Scopes**: Permissions attached to a key limiting which resources and actions it can access.
- **Rate Limiting**: Restricting the number of requests per key within a time window.
- **Key Rotation**: Replacing an active key with a new one, with a grace period where both keys work.
1. **Hash API keys before storage** using Argon2id or bcrypt. Store only the hash — show the plaintext exactly once at creation.
2. **Use identifiable prefixes** (e.g., `sk_live_`, `sk_test_`, `pk_`) so that leaked keys can be detected by automated secret scanners.
3. **Scope every key** with fine-grained permissions. Avoid all-access keys.
4. **Implement key rotation** with a grace period where both old and new keys are valid, so consumers can update without downtime.
5. **Track `lastUsedAt`** for every key. Alert on or auto-revoke keys that have not been used in 90+ days.
6. **Set expiration dates** by default. Long-lived keys should require explicit opt-in.
skilldb get auth-patterns-skills/API Key AuthFull skill: 222 lines
Paste into your CLAUDE.md or agent config

API Key Authentication — Authentication & Authorization

You are an expert in API key authentication for securing programmatic access to services. You understand key generation, secure storage, scope-based permissions, rotation strategies, and rate limiting.

Core Philosophy

Overview

API key authentication identifies and authorizes machine-to-machine or developer-to-service communication using a bearer token (the API key). Unlike user-session or OAuth-based auth, API keys typically represent a service account, project, or integration rather than an individual user. API keys are simpler than OAuth but offer fewer security guarantees — they are long-lived, often lack audience binding, and are easily leaked. Proper generation, storage, scoping, and rotation practices are essential.

Core Concepts

  • API Key: A randomly generated string used as a bearer credential in API requests.
  • Key Prefix: A short, human-readable prefix (e.g., sk_live_) that identifies the key type and environment without revealing the secret.
  • Key Hash: The server stores only a hash of the key; the plaintext is shown to the user once at creation.
  • Scopes: Permissions attached to a key limiting which resources and actions it can access.
  • Rate Limiting: Restricting the number of requests per key within a time window.
  • Key Rotation: Replacing an active key with a new one, with a grace period where both keys work.

Implementation Patterns

Key Generation and Storage

const crypto = require('crypto');
const argon2 = require('argon2');

async function generateApiKey(userId, name, scopes) {
  // Generate a 32-byte random key
  const rawKey = crypto.randomBytes(32).toString('base64url');

  // Add a prefix for identification (never hash the prefix)
  const prefix = 'sk_live_';
  const fullKey = prefix + rawKey;

  // Store only the hash — the plaintext is returned once and never stored
  const keyHash = await argon2.hash(rawKey, {
    type: argon2.argon2id,
    memoryCost: 19456,
    timeCost: 2,
    parallelism: 1,
  });

  // Store a short suffix for display (last 4 characters)
  const displaySuffix = rawKey.slice(-4);

  await db.apiKeys.create({
    userId,
    name,
    prefix,
    keyHash,
    displayHint: `${prefix}...${displaySuffix}`,
    scopes,
    createdAt: new Date(),
    lastUsedAt: null,
    expiresAt: null,  // Or set a default expiry
    revoked: false,
  });

  // Return the full key — this is the ONLY time it will be visible
  return { key: fullKey, displayHint: `${prefix}...${displaySuffix}` };
}

Key Verification Middleware

async function authenticateApiKey(req, res, next) {
  const header = req.headers.authorization;
  if (!header?.startsWith('Bearer sk_')) {
    return res.status(401).json({ error: 'API key required' });
  }

  const fullKey = header.slice(7); // Remove 'Bearer '
  const prefix = fullKey.slice(0, 8); // 'sk_live_'
  const rawKey = fullKey.slice(8);

  // Look up candidate keys by prefix (narrows the search)
  const candidates = await db.apiKeys.find({
    prefix,
    revoked: false,
    $or: [{ expiresAt: null }, { expiresAt: { $gt: new Date() } }],
  });

  // Verify against stored hashes
  let matchedKey = null;
  for (const candidate of candidates) {
    if (await argon2.verify(candidate.keyHash, rawKey)) {
      matchedKey = candidate;
      break;
    }
  }

  if (!matchedKey) {
    return res.status(401).json({ error: 'Invalid API key' });
  }

  // Update last-used timestamp (non-blocking)
  db.apiKeys.updateOne(
    { _id: matchedKey._id },
    { $set: { lastUsedAt: new Date() } }
  ).catch(() => {});

  req.apiKey = matchedKey;
  req.apiKeyScopes = new Set(matchedKey.scopes);
  next();
}

// Scope check
function requireScope(...requiredScopes) {
  return (req, res, next) => {
    for (const scope of requiredScopes) {
      if (!req.apiKeyScopes.has(scope)) {
        return res.status(403).json({ error: `Missing scope: ${scope}` });
      }
    }
    next();
  };
}

// Usage
app.get('/api/v1/data', authenticateApiKey, requireScope('data:read'), getData);
app.post('/api/v1/data', authenticateApiKey, requireScope('data:write'), createData);

Key Rotation with Grace Period

app.post('/api/keys/:keyId/rotate', authenticate, async (req, res) => {
  const oldKey = await db.apiKeys.findOne({
    _id: req.params.keyId,
    userId: req.user.id,
  });

  if (!oldKey) return res.status(404).json({ error: 'Key not found' });

  // Generate new key with same scopes and name
  const newKeyResult = await generateApiKey(
    req.user.id,
    oldKey.name,
    oldKey.scopes
  );

  // Set the old key to expire after a grace period (e.g., 24 hours)
  await db.apiKeys.updateOne(
    { _id: oldKey._id },
    { $set: { expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) } }
  );

  res.json({
    newKey: newKeyResult.key,
    oldKeyExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
    message: 'Old key will remain valid for 24 hours',
  });
});

Rate Limiting by API Key (Redis)

const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

async function rateLimitByApiKey(req, res, next) {
  const keyId = req.apiKey._id.toString();
  const window = 60; // 1 minute
  const limit = req.apiKey.rateLimit || 100; // Default 100 req/min

  const key = `ratelimit:${keyId}:${Math.floor(Date.now() / 1000 / window)}`;
  const count = await redis.incr(key);

  if (count === 1) {
    await redis.expire(key, window);
  }

  res.set('X-RateLimit-Limit', limit);
  res.set('X-RateLimit-Remaining', Math.max(0, limit - count));

  if (count > limit) {
    return res.status(429).json({ error: 'Rate limit exceeded', retryAfter: window });
  }

  next();
}

Best Practices

  1. Hash API keys before storage using Argon2id or bcrypt. Store only the hash — show the plaintext exactly once at creation.
  2. Use identifiable prefixes (e.g., sk_live_, sk_test_, pk_) so that leaked keys can be detected by automated secret scanners.
  3. Scope every key with fine-grained permissions. Avoid all-access keys.
  4. Implement key rotation with a grace period where both old and new keys are valid, so consumers can update without downtime.
  5. Track lastUsedAt for every key. Alert on or auto-revoke keys that have not been used in 90+ days.
  6. Set expiration dates by default. Long-lived keys should require explicit opt-in.
  7. Rate limit per key and return standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining).
  8. Log key usage with the key ID (never the key itself) for audit trails.

Common Pitfalls

  • Storing API keys in plaintext: If the database is breached, every key is compromised. Always hash.
  • Logging the full API key: Logs are often broadly accessible. Log only the key ID or display hint.
  • No key scoping: A single all-powerful key means any compromised integration has full access to the entire API.
  • Embedding keys in client-side code: API keys in JavaScript bundles or mobile apps are extractable. Use server-side proxies or OAuth for client-facing access.
  • No revocation mechanism: If a key is leaked and cannot be revoked, the only option is to change the entire authentication scheme.
  • Treating API keys as user authentication: API keys identify a service or integration, not a person. Do not use them for user login flows.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add auth-patterns-skills

Get CLI access →