From 946436fdafe8cb54dad7329634a7feae99d52a00 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 26 Apr 2026 20:37:33 +0200 Subject: [PATCH] docs(adr): add ADR 0008 (HMAC-SHA256 API keys) and 0009 (Application to MySQL) --- .../0008-hmac-sha256-api-key-hashing.md | 63 +++++++++++++++++++ ...09-application-model-migration-to-mysql.md | 58 +++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 docs/decisions/0008-hmac-sha256-api-key-hashing.md create mode 100644 docs/decisions/0009-application-model-migration-to-mysql.md diff --git a/docs/decisions/0008-hmac-sha256-api-key-hashing.md b/docs/decisions/0008-hmac-sha256-api-key-hashing.md new file mode 100644 index 0000000..b4be8a3 --- /dev/null +++ b/docs/decisions/0008-hmac-sha256-api-key-hashing.md @@ -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. diff --git a/docs/decisions/0009-application-model-migration-to-mysql.md b/docs/decisions/0009-application-model-migration-to-mysql.md new file mode 100644 index 0000000..50b6e0c --- /dev/null +++ b/docs/decisions/0009-application-model-migration-to-mysql.md @@ -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.