docs(adr): add ADR 0008 (HMAC-SHA256 API keys) and 0009 (Application to MySQL)

This commit is contained in:
2026-04-26 20:37:33 +02:00
parent a27b8018b1
commit 946436fdaf
2 changed files with 121 additions and 0 deletions
@@ -0,0 +1,63 @@
# 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 `SECRET` environment variable remaining confidential. If it leaks, an attacker can pre-compute hashes for any key. `SECRET` must be rotated and all API keys regenerated if a leak is suspected.
* The same `SECRET` is used for JWT signing and HMAC — a future improvement would be to use a dedicated `APIKEY_SECRET` env 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-headerapikey` strategy.
* Related to [ADR 0006](0006-jwt-authentication.md) — both auth mechanisms share the `SECRET` env var.