Commit Graph
2 Commits
Author SHA1 Message Date
Lucas RayanandClaude Opus 5 833dc76c2e #21: make the auth service actually authenticate
The F4 auth service issued a valid session for any email and any password,
and accepted any non-empty string as a second factor. Reproduced against
the router before touching it: an invented password plus the code "000000"
returned a SUPER_ADMIN token that opened /api/v1/admin/dashboard.

Passwords. Hashing moves from PBKDF2 to Argon2id via hash-wasm, which is
what PLAN.md section 9 and the Go service already use - the previous
scheme meant a password set through the mail stack could never verify
here. Interoperability was checked in both directions before adopting the
library: a hash written by alexedwards/argon2id verifies in Node and one
written here verifies in Go. hash-wasm is pure WASM, so the multi-arch
Alpine images need no native build.

verifyPassword no longer falls back to comparing the password against the
stored hash. That fallback turned every stored hash into a working
credential: anyone with a database dump could authenticate by sending the
hash back. Anything that is not an argon2id PHC string is now refused.

Logins are decided against the database, with the failure counter and the
temporary lockout of PLAN.md section 5.2. An unknown address and a wrong
password give the same answer, and the unknown case still pays for a hash
so response time does not enumerate valid addresses.

Second factor. The TOTP secret is generated server-side, stored encrypted
and never accepted back from the client - the confirm step used to take
whichever secret the caller sent, so anyone could confirm with their own.
Recovery codes are Argon2id, single-use, and consumed under a guard so two
concurrent requests cannot spend the same one. App passwords are issued
and revoked through real endpoints instead of browser state.

Sessions gain a "purpose" claim. Without it the challenge token - handed
out after the password but before the second factor - satisfied every
check a real session did, which skipped MFA entirely.

The dev seed carried a hand-written password_hash whose digest was 24
bytes instead of 32, so it matched no password at all. Nothing noticed
while passwords went unverified; it is a real hash now.

Verified with 30 tests, 13 of them against a real Postgres: wrong password,
unknown account, pass-the-hash, wrong TOTP code, challenge token replay,
lockout, single-use recovery codes, and the surface isolation that F4's
acceptance criteria require.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 09:33:29 -03:00
Lucas Rayan e501cdc69e #2: implement F1 sub-project 1 - inbound SMTP listener with EML storage
Adds a real :25 SMTP listener to the Go protocols service, giving F1's
"Nucleo SMTP" milestone its first working sub-project: application,
infrastructure, and presentation layers on top of F0's domain-only
Clean Architecture.

- ProcessInboundEmailUseCase resolves recipients against Postgres and
  persists accepted mail (blob dedup by content hash, transactional
  UID allocation + outbox event, never sending 250 OK before durable
  commit per PLAN.md section 6.1).
- Postgres repositories via pgx/v5 (replacing lib/pq entirely).
- Local-disk blob storage with content-hash dedup, rename-before-commit
  ordering so a DB row can never reference a file that isn't actually
  on disk.
- A local-file TLS cert provider for STARTTLS, with an ephemeral
  self-signed fallback so a missing/misconfigured cert degrades
  gracefully instead of crash-looping the service (PLAN.md section 8.4).
- A github.com/emersion/go-smtp Backend/Session adapting real SMTP
  traffic to the use case: unknown recipients get 550 5.1.1 before
  DATA, duplicate RCPT TO for the same address is deduplicated, TLS is
  hardened to the project's required PFS-only cipher/curve policy.
- A dev-seed script and Makefile target for local testing.

Explicitly out of scope (separate follow-up sub-projects): alias
resolution, SPF/DKIM/DMARC/ARC verification, Rspamd/ClamAV, Submission
ports with SASL auth, DKIM signing, outbound delivery, and the real
Traefik acme.json cert watcher.

Verified live end-to-end via WSL Docker: the full compose stack (db,
redis, migrate, auth, storage, protocols, web) reaches healthy, and a
real swaks SMTP session is accepted and durably persisted with correct
sequential UIDs, while an unknown recipient is correctly rejected with
550 5.1.1 and zero rows persisted. Built via 10 sequential tasks under
subagent-driven development, each independently reviewed, plus a final
whole-branch review that caught and fixed a TLS-crash bug, a blob
commit-ordering bug, missing TLS hardening, and missing CI integration
coverage before merge.

Known follow-up, not blocking: email_messages' partition only covers
July 2026 and needs a partition-management job before 2026-08-01.
2026-07-23 08:41:31 -03:00