STORY-F-006: phx.gen.auth scaffold + Argon2 + TOTP MFA¶
Epic: Auth Priority: Must Have Story Points: 3 Status: Not Started Assigned To: Unassigned Created: 2026-04-17 Sprint: 2
User Story¶
As a user of any role, I want to log in with email + password (with optional TOTP MFA) and have my session secured by Phoenix conventions, so that authenticated access is possible from Sprint 2 onwards and my password is never stored in plaintext.
Description¶
Background¶
PRD E1.3 specifies Phoenix-native auth with Argon2 hashing (SE-08), optional TOTP MFA, and mandatory FIDO2 for admin/payroll/director roles. FIDO2 WebAuthn is complex — defer to a later sprint (during Scout + Verify go-live). This story delivers email + password + TOTP MFA as a baseline. FIDO2 gets its own story in Sprint 7–8 or Migration Phase 1.
AgenticAI-app uses phx.gen.auth — carry-forward pattern. Gautham is familiar; this is largely generated code.
Scope¶
In scope:
- Run
mix phx.gen.auth Accounts User users --webequivalent manually (umbrella shape requires care — scaffold generates intofinnest_webwith Repo fromfinnest_core) Finnest.Core.Users.UserEcto schema with: id UUID, org_id UUID (FK), email citext, hashed_password, confirmed_at, totp_secret_encrypted (Cloak-encrypted), inserted_at, updated_at, deleted_at- Note:
public.usersin the architecture hasroleand more fields — F-007 expands this schema; F-006 lands the minimum for phx.gen.auth to work - Argon2 hashing via
{:argon2_elixir, "~> 4.0"}(SE-08) - Registration, login, logout, email confirmation, password reset routes under
/users/* - Email delivery via
Swoosh.Adapters.Localin dev (Mailpit); SES in prod —CommEmailport stubs added, real port implementation deferred - TOTP MFA (optional): user enables TOTP → QR code shown → scan in authenticator app → verify →
totp_secret_encryptedwritten; subsequent logins require 6-digit code - Session timeout: 60 min rolling (commercial default). IRAP override to 15 min deferred to IRAP phase.
- Lockout after 5 failed login attempts in 10 min (account lockout for 15 min) — E1.3 AC4
- Every auth event emits
user_registered/user_logged_in/user_logged_out/mfa_enabled/password_reset_requested/account_lockedevents — event store (F-016) not ready yet, so buffer in memory / Oban and re-play when F-016 lands, OR write to a temporarypublic.audit_logtable and migrate
Out of scope:
- FIDO2 / WebAuthn (deferred to PRD Scout/Verify sprints or Migration Phase 1)
- M365 SSO (deferred — PRD E1.3 AC6 lands in Migration Phase 1)
public.users.role+ full schema (expanded in F-007)- Per-org lockout policies, admin-visible audit of auth events (F-007 + F-016)
- Account deletion cascade (PRD E1.7 AC8 — Migration Phase ½)
Technical Notes¶
phx.gen.authgenerates intofinnest_web— acceptable per architecture (web edge can call any domain), but the User schema must live infinnest_core(foundational). Adjust the generated code to split schema (infinnest_core/lib/finnest/core/users/user.ex) vs context (finnest_core/lib/finnest/core/users.ex) vs LiveView + controllers (infinnest_web).- Cloak encryption for
totp_secret_encrypted: useFinnest.Core.Vault.BinaryEcto type (implement in F-007 when schema fully lands, OR stub in F-006 with plain BYTEA + @TODO Cloak in F-007) - Session storage: default Plug session with signed cookie — fine for Sprint 2. Upgrade to DB-backed session store later if needed.
- UI: login/signup/reset pages styled with DaisyUI (F-004 asset pipeline). Accessible (WCAG AA): semantic form, labels, aria-describedby for errors.
- Rate limit
/users/log_inat 5 attempts / 10 min per email + 20 attempts / hour per IP — Hammer library (defer full Hammer setup to F-020; use in-memory token bucket for F-006) - Lockout: new
users.locked_untilcolumn; login pipeline checks; auto-unlock on expiry
Dependencies¶
- Blocked by: STORY-F-003 (Repo), STORY-F-004 (web endpoint)
Acceptance Criteria¶
- User can register with email + password; email confirmation link sent (Mailpit in dev)
- Confirmed user can log in; session persists 60 min rolling
- Logout clears session;
GET /shows unauthenticated state - Password reset flow works end-to-end (request → email → click link → new password)
- User can enable TOTP MFA via
/users/mfa; QR code displayed; verification via 6-digit code - After enabling MFA, login requires TOTP code; wrong code → error, rate-limited
- 5 failed login attempts in 10 min → account locked until
locked_until; attempted login returns "account locked" error (not "invalid password" — specific message per E1.3 AC4) - Passwords hashed with Argon2 (assert in repo query:
User.hashed_passwordstarts with$argon2) - TOTP secret encrypted at rest (BYTEA column populated; not plaintext)
- Auth events emitted (to temporary audit_log table until F-016) with correlation_id + user_id
- Security headers present on auth pages (inherited from F-004 plug stack)
-
mix format,credo --strict,dialyzerall green
Testing Requirements¶
- Registration flow: ExUnit + Phoenix ConnTest + LiveView test end-to-end
- Login flow: valid + invalid password + locked account + MFA required
- MFA setup: QR code rendered, TOTP code validation, wrong code rejection
- Password reset: expired token, used token, valid token
- Rate limiting: 5 rapid failed logins → 6th locked; 15-min auto-unlock test
- Security: attempt SQL injection in email field → Changeset rejects
- Coverage ≥80% for the Users context
References¶
- PRD
../prd/prd-scout-verify-golive.mdE1.3 ../architecture/architecture.md§Authentication../10-GUARDRAILS.mdSE-08, SE-01, SE-05, AR-10, Commandment #27