3.4 KiB
Use HMAC-SHA256 for API key hashing
- Status: accepted
- Date: 2026-04-26
Context and Problem Statement
API keys must be stored hashed in the database (never in plaintext). Authentication requires looking up the corresponding Application record by the hash of the incoming key. This makes the hashing algorithm subject to an unusual constraint: the hash must be deterministic so that the same key always produces the same hash and the database lookup works.
Decision Drivers
- Deterministic hashing is required to enable
findOne({ apikey: hash })lookups. - The previous implementation used bcrypt with the JWT secret as a fixed salt — this made hashes deterministic but defeated bcrypt's core security property (random salting).
- API keys are UUID v4 values (128 bits of entropy) — they are not low-entropy secrets like passwords.
- A slow hashing function creates a DoS vector: an attacker flooding the API key auth endpoint forces expensive bcrypt operations on every request.
Considered Options
- bcrypt with random salt (standard password hashing)
- bcrypt with fixed salt (previous implementation)
- HMAC-SHA256 with the application secret
Decision Outcome
Chosen option: "HMAC-SHA256", because it is deterministic (lookup by hash works), fast (no DoS surface), and cryptographically appropriate for high-entropy secrets. The security guarantee shifts from computational hardness (bcrypt) to secret confidentiality (HMAC key), which is the correct model for API keys.
Implementation: crypto.createHmac('sha256', secret).update(key).digest('hex') in src/middlewares/auth.js (key generation) and src/config/passport-headerapikey.js (verification). The HMAC key is the application SECRET environment variable, shared with JWT signing.
Positive Consequences
- Database lookup by hash remains possible — no change to the query pattern.
- No DoS vector: HMAC-SHA256 is fast (~microseconds vs bcrypt's ~100ms).
- bcrypt's fixed-salt weakness eliminated.
Negative Consequences
- Security depends on the
SECRETenvironment variable remaining confidential. If it leaks, an attacker can pre-compute hashes for any key.SECRETmust be rotated and all API keys regenerated if a leak is suspected. - The same
SECRETis used for JWT signing and HMAC — a future improvement would be to use a dedicatedAPIKEY_SECRETenv var.
Pros and Cons of the Options
HMAC-SHA256
- Good, because deterministic — lookup by hash works without additional query logic.
- Good, because fast — no DoS risk on the auth endpoint.
- Good, because appropriate for high-entropy inputs (UUID keys have 128 bits of entropy; bcrypt's brute-force resistance is unnecessary).
- Bad, because security depends on secret confidentiality rather than computational cost.
bcrypt with random salt
- Good, because industry standard for secrets that require brute-force resistance.
- Bad, because non-deterministic — lookup by hash is impossible without storing additional plaintext identifiers.
bcrypt with fixed salt (previous implementation)
- Good, because deterministic.
- Bad, because a fixed salt makes all hashes pre-computable for a given salt — defeating bcrypt's main purpose.
- Bad, because bcrypt's slowness creates a DoS surface on the auth endpoint.
Links
- Replaces the fixed-salt bcrypt approach introduced with
passport-headerapikeystrategy. - Related to ADR 0006 — both auth mechanisms share the
SECRETenv var.