Generating Secure API Keys: A Q&A for Backend Developers

Generating Secure API Keys: A Q&A for Backend Developers

I've been on the receiving end of a lot of code reviews where someone has quietly slipped in Math.random() to generate an API key. No malice — just a gap in understanding. This Q&A exists because these questions come up constantly, and the answers matter. A weak API key isn't a hypothetical risk; it's a door left open.


Q: What exactly is "entropy," and why does everyone bring it up when discussing API keys?

Entropy, in this context, is a measure of unpredictability. If I can guess your API key with any reasonable probability, your key has low entropy. If it would take a modern cluster of GPUs until the heat death of the universe to brute-force it, you have high entropy.

The practical point: entropy is not just about how long your key string is. A 64-character key built from only four possible characters ("a", "b", "c", "d") is far weaker than a 32-character key drawn from a cryptographically random source using the full alphanumeric alphabet. The mathematical entropy of the former is roughly 128 bits on paper but 0 bits in practice if an attacker figures out your alphabet.

When people say "use 128 bits of entropy," they mean: generate 128 bits from a cryptographically secure source and then encode it — not that your final string needs to be 128 characters long.


Q: What's the actual problem with Math.random()?

The problem is that Math.random() — in JavaScript and in most language equivalents like Python's random.random() — is a pseudorandom number generator (PRNG) designed for speed and statistical distribution, not security. These generators are seeded from a relatively small set of values (often system time) and are algorithmically predictable once you know or can guess the seed.

There's been documented real-world exploitation of this. In 2016, researchers showed that session tokens generated by certain versions of PHP's mt_rand() could be predicted after observing a handful of tokens — because Mersenne Twister, which powers mt_rand(), has internal state that leaks through its output. An attacker who sees even a few tokens can reconstruct the generator state and predict future values.

If your API keys are generated with Math.random() or equivalent, someone who requests a few free-tier tokens and does some math could predict your next issued key. That's not a theoretical threat.


Q: So what should I use instead?

A Cryptographically Secure Pseudorandom Number Generator (CSPRNG). These are designed so that knowing past outputs gives you no useful information about future outputs. The seed is sourced from the operating system's entropy pool — hardware noise, interrupt timing, network activity — not from something predictable like the clock.

Language-specific options:

  • Node.js: crypto.randomBytes(n) — built-in, no dependencies. Returns a Buffer of n cryptographically random bytes.
  • Python: secrets.token_bytes(n) or secrets.token_urlsafe(n) — the secrets module was added in 3.6 specifically to replace uses of random in security contexts.
  • Go: crypto/rand.Read() — use this, not math/rand.
  • Ruby: SecureRandom.hex(n) or SecureRandom.urlsafe_base64(n).
  • Java: java.security.SecureRandom — but be careful; instantiating it repeatedly can be expensive. Create one instance and reuse it.

In Node.js, generating a 32-byte key looks like this:

const crypto = require('crypto');
const apiKey = crypto.randomBytes(32).toString('hex'); // 64 hex chars = 256 bits

In Python:

import secrets
api_key = secrets.token_urlsafe(32)  # ~43 URL-safe chars, 256 bits of entropy

Q: How long should an API key be?

128 bits of entropy is the widely accepted minimum for long-lived credentials. 256 bits is better and cheap — generating an extra 16 bytes costs you nothing meaningful in performance.

What does that look like in practice? 16 bytes = 128 bits = 32 hex characters. 32 bytes = 256 bits = 64 hex characters, or about 43 characters in URL-safe Base64.

Avoid making keys shorter "for usability." Developers copy-paste keys; they don't type them. A 64-character hex string is no more painful than a 32-character one for the person integrating your API. But the security difference is meaningful when you consider a determined, resourced attacker.

One caveat: don't confuse key length with key entropy. A UUID (v4) is 128 bits total but only 122 bits are random — the version and variant nibbles are fixed. That's still fine for most purposes, but it's worth knowing if you're comparing options.


Q: Should API keys use UUID v4, or is custom generation better?

UUID v4 is a legitimate option — it's built on 122 bits of random data, tools exist everywhere to generate it, and it gives you a consistent format. Libraries like uuid in Node.js or Python's built-in uuid.uuid4() use CSPRNGs under the hood.

However, raw UUIDs have one practical downside: they're not prefixed, so you can't immediately tell from a string that it's an API key. This matters during log analysis, secret scanning, and debugging. A raw UUID like f47ac10b-58cc-4372-a567-0e02b2c3d479 is indistinguishable from a database record ID in your logs.

Many teams now use prefixed keys — Stripe's format is a good reference: sk_live_... or pk_test_.... The prefix encodes the key type and environment, which means you can grep logs meaningfully and automated secret scanners (like GitHub's push protection) can be trained to recognize your format.

A reasonable custom format: {prefix}_{base62_or_urlsafe_base64_of_random_bytes}. Example:

// Node.js
function generateApiKey(prefix = 'myapp') {
  const randomPart = crypto.randomBytes(24).toString('base64url');
  return `${prefix}_${randomPart}`;
}
// Output: myapp_3Df9aKv2QNLzXpRsYcBwH1mT

Just ensure your prefix is documented and consistent. The random part should still be at least 128 bits.


Q: How should I store API keys on the server side?

This trips up a lot of developers. API keys should be stored hashed — specifically, as a fast-but-secure hash like SHA-256 — not encrypted and definitely not in plaintext.

The reasoning: if your database is compromised, a plaintext or reversibly-encrypted key list is a complete breach. With hashing, an attacker gets hashes, not keys. Unlike passwords, you don't need bcrypt or Argon2 here (those are slow by design to resist brute-force against short, human-chosen passwords). API keys have enough entropy that SHA-256 is sufficient — a 256-bit random key has no brute-force vulnerability even against a fast hash function.

The tradeoff: you can only show the full key to the user once, at creation time. This is exactly the behavior you see with Stripe, GitHub, and others — generate, display once, never again. Store only the hash plus a short prefix (like the first 8 characters) to help users identify which key is which in a list.

const crypto = require('crypto');

function hashApiKey(rawKey) {
  return crypto.createHash('sha256').update(rawKey).digest('hex');
}

// On creation: show rawKey to user, store hashApiKey(rawKey) in DB
// On request: hash the incoming key and compare to stored hash

Q: What's the right approach to key rotation?

Key rotation means issuing a new key and invalidating the old one — it's a core part of credential hygiene. There are two scenarios to design for.

Planned rotation: Allow users to generate a new key while the old one remains valid for a configurable grace period (24–72 hours is common). This gives teams time to update their integrations without a sudden outage. Your system needs to support multiple active keys per account during the overlap window.

Emergency rotation: When a key may be compromised — showed up in a log, accidentally committed to a repo — you need to invalidate the old key immediately and issue a new one. No grace period. This requires that your key lookup is fast (indexed hash in the database) and that your revocation takes effect within one request cycle, not after a cache TTL.

Build rotation as a first-class feature, not an afterthought. Provide a UI button and an API endpoint for it. Inform users via email when a key is rotated (especially on emergency rotation). Log all rotation events in your audit log with timestamps and the initiating actor.

One build/CI tip: many teams now run secret-scanning tools (Gitleaks, TruffleHog, GitHub Advanced Security) in their CI pipelines. If you use prefixed keys, you can add a custom regex pattern to these tools so any accidental commit of your API keys is caught before merge. Add that to your CI step alongside your linting and tests.


Q: Any quick checklist before shipping an API key system?

  • Generated using a CSPRNG — not Math.random, not UUID from an insecure source.
  • At least 128 bits of entropy, preferably 256.
  • Prefixed so they're identifiable in logs and scannable by automated tools.
  • Stored as SHA-256 hashes, displayed to users exactly once.
  • Revocable immediately — indexed hash lookup, no long cache TTL on auth.
  • Rotation supported with grace period for planned rotations.
  • Secret scanning pattern added to your CI pipeline.
  • Audit log entries on creation, use, and revocation.

None of these are exotic. They're table stakes for anything you're putting in front of real users. Getting them right the first time is a lot cheaper than rotating credentials for an entire user base after an incident.