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.
@@ -0,0 +1,58 @@
# Migrate Application model from MongoDB to MySQL
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The `Application` model (API key management) was stored in MongoDB while user identity (`User` model, JWT auth) was stored in MySQL. This split the authentication layer across two databases: verifying who a request comes from required MongoDB for API key auth and MySQL for JWT auth. The two-database architecture is otherwise intentional (see [ADR 0002](0002-database-migration-mongodb-to-mysql.md)) but the auth layer is a special case where consistency matters for security auditability and operational simplicity.
## Decision Drivers
* Auth-related models should be co-located to allow transactional integrity (e.g. cascading deletes when a user is removed).
* Security audits are easier when all auth data lives in one queryable store.
* `Application.authorId` references a MySQL `User.id` — a foreign key that MongoDB cannot enforce.
* The skydive and Hero Wars domains intentionally remain on MongoDB; `Application` is not part of those domains.
## Considered Options
* Keep `Application` on MongoDB
* Migrate `Application` to MySQL
## Decision Outcome
Chosen option: "Migrate Application to MySQL", because the auth layer (JWT + API key) is now consolidated on a single database with enforced referential integrity. The skydive and Hero Wars MongoDB collections are unaffected.
Migration implemented via Sequelize migration `20260426000000-add-application.js`. The Mongoose model was deleted and replaced with a Sequelize model in `src/database/models/mysql/Application.js`. The `User hasMany Application` association is declared in `src/database/relationships/index.js`.
### Positive Consequences
* Auth layer fully on MySQL — a single connection pool and query interface for all auth operations.
* `authorId` foreign key enforced at the database level (`ON DELETE SET NULL`).
* Consistent UUID v4 primary keys across all auth models.
* API key hashing algorithm change (ADR 0008) applied cleanly on the new model.
### Negative Consequences
* Existing API key hashes stored in MongoDB are invalidated by the migration (different hashing algorithm). Applications must be recreated via the app to generate new keys.
* Requires a Sequelize migration to create the `application` table before deploying.
## Pros and Cons of the Options
### Keep Application on MongoDB
* Good, because no migration effort.
* Bad, because auth data is split across two databases — harder to audit and no referential integrity with `User`.
* Bad, because `authorId` is an unenforceable soft reference to a MySQL row.
### Migrate Application to MySQL
* Good, because auth layer is consolidated — simpler to audit and operate.
* Good, because foreign key to `User` can be enforced.
* Good, because consistent primary key format (UUID v4) across all MySQL models.
* Bad, because existing MongoDB API key records are invalidated and must be recreated.
## Links
* Related to [ADR 0002](0002-database-migration-mongodb-to-mysql.md) — general migration strategy (MongoDB → MySQL for CMS/ecommerce domains).
* Related to [ADR 0008](0008-hmac-sha256-api-key-hashing.md) — API key hashing algorithm applied on this model.