Hashing API Keys with Bcrypt

Learn how to securely store API keys using bcrypt hashing. Understand why API keys should be hashed like passwords, lookup strategies using key prefixes, and when bcrypt is or is not the right choice.

Best Practices

Detailed Explanation

Hashing API Keys with Bcrypt

API keys are secrets, just like passwords. If an attacker gains access to your database, plaintext API keys give them immediate access to every integrated service and user account. Hashing API keys with bcrypt provides the same protection as hashing passwords.

Why Hash API Keys?

Most applications store API keys in plaintext:

-- Insecure: plaintext API keys
SELECT * FROM api_keys WHERE key = 'sk_live_abc123def456...';

If this database is breached, every API key is immediately compromised. Hashing prevents this:

-- Secure: only hashed keys stored
-- key_prefix used for lookup, bcrypt_hash for verification
SELECT * FROM api_keys WHERE key_prefix = 'sk_live_abc1';

The Prefix + Hash Strategy

Since bcrypt hashes cannot be used for direct lookups (different salt = different hash for the same input), use a prefix-based lookup:

  1. Generate a random API key: sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4
  2. Store the first 8 characters as a lookup prefix: sk_live_a1
  3. Hash the full key with bcrypt: bcrypt.hash(fullKey, 12)
  4. Show the full key to the user once (they must store it)

Lookup and verification:

async function verifyApiKey(providedKey) {
  const prefix = providedKey.substring(0, 10);
  const candidates = await db.query(
    'SELECT * FROM api_keys WHERE key_prefix = $1',
    [prefix]
  );

  for (const candidate of candidates) {
    if (await bcrypt.compare(providedKey, candidate.bcrypt_hash)) {
      return candidate; // Valid key found
    }
  }
  return null; // No match
}

Prefix Length Considerations

The prefix must be long enough to narrow down candidates but short enough to not be useful to an attacker:

  • Too short (4 chars): many collisions, slow lookups
  • Good (8–10 chars): typically 1–2 candidates per lookup
  • Too long (20+ chars): effectively stores a significant portion of the key in plaintext

Performance Considerations

Bcrypt is slow by design. For API keys that are verified on every request (not just login), the ~15 ms overhead of cost 12 may be significant:

  • High-traffic APIs (>1,000 req/s): consider lower cost (8–10) or caching verified keys in memory with a short TTL
  • Low-traffic APIs: cost 12 is fine
  • Alternative: use SHA-256 for API keys (acceptable since API keys are random, not user-chosen, and resistant to dictionary attacks)

When Not to Use Bcrypt for API Keys

Bcrypt’s 72-byte limit means long API keys are truncated. If your API keys exceed 72 bytes, either:

  • Use SHA-256 instead (acceptable for high-entropy random keys)
  • Pre-hash with SHA-256 before bcrypt (like the password pre-hashing technique)

Database Schema

CREATE TABLE api_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  key_prefix VARCHAR(10) NOT NULL,
  bcrypt_hash VARCHAR(60) NOT NULL,
  name VARCHAR(255),
  scopes TEXT[],
  created_at TIMESTAMP DEFAULT NOW(),
  last_used_at TIMESTAMP,
  expires_at TIMESTAMP
);

CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix);

Key Rotation

When a user rotates their API key, generate a new random key, hash it, and store the new prefix and hash. Optionally keep the old key active for a grace period to prevent service disruptions.

Use Case

API key security is critical for SaaS platforms, payment processors, and any service that issues programmatic access credentials. Many high-profile breaches have exposed plaintext API keys stored in databases. Hashing API keys follows the same security principle as hashing passwords: defense in depth against database breaches. The prefix-based lookup strategy solves the unique challenge of searching for a hashed value.

Try It — Bcrypt Generator

Open full tool