feat(auth): finish PMS-4 story (mailer, MFA TOTP, API keys) #20

Closed
David wants to merge 0 commits from feat/auth-story-pms-4 into main
Owner

Implements the bulk of YouTrack story PMS-4 (auth + user management). One commit per sub-task.

Sub-tasks

  • PMS-5 list_users real query: already shipped in the audit-batch series; closed administratively.
  • PMS-6 login rate-limit: already shipped via src/modules/auth/rate_limit.rs; closed administratively.
  • PMS-7 password reset email: new utils::email::Mailer (LogMailer dev, SmtpMailer prod via lettre). Selected by SMTP_HOST presence. Wired into AuthService::request_password_reset. Frontend link is built from CLIENT_ORIGIN.
  • PMS-8 welcome email on user create: mints a 7-day password-reset token and emails the new user a /reset-password/<token> link, reusing the existing reset machinery rather than introducing a parallel setup-token table.
  • PMS-9 MFA TOTP: real verification in the login flow against users.mfa_secret (base32) via mokosh_auth_crypto::totp::verify (+-1 step skew). Three new endpoints: POST /me/mfa/setup (stage secret + provisioning URI), POST /me/mfa/enable (confirm one code, flip flag), POST /me/mfa/disable (clear after password re-auth).
  • PMS-10 personal API keys: POST /me/api-keys, GET /me/api-keys, DELETE /me/api-keys/:id. Raw key returned once on create; DB stores only key_prefix (10-char index) + argon2 hash.

Behaviour-visible changes

  • AuthService::new is preserved; new AuthService::with_mailer is the production constructor (called by create_api_router).
  • create_api_router gains a final mailer: Arc<dyn Mailer> parameter; main.rs builds it from MailerConfig::from_env. The boot path hard-fails on misconfiguration (e.g. SMTP_USERNAME without SMTP_PASSWORD) rather than silently downgrading to LogMailer.
  • users.mfa_secret is now read/written on enrollment and consulted on login when mfa_enabled = true. No schema change required.
  • api_keys rows are created on POST /me/api-keys; no bearer-auth middleware change yet (kept out of scope; tracked separately).

Test plan

  • cargo check --bin mokosh-server clean (verified locally).
  • Smoke: POST /api/v1/auth/forgot-password for a known user logs the reset link (LogMailer dev) or sends via SMTP (compose.dev.yml mailpit at http://localhost:8025).
  • Smoke: POST /api/v1/auth/me/mfa/setup returns secret + URI; an authenticator app loaded with the URI produces codes that POST /me/mfa/enable accepts; subsequent login requires the code.
  • Smoke: POST /api/v1/auth/me/api-keys returns a psa_... key; GET lists with prefix only; DELETE removes it.

Closes #PMS-4

Implements the bulk of YouTrack story PMS-4 (auth + user management). One commit per sub-task. ## Sub-tasks - PMS-5 `list_users` real query: already shipped in the audit-batch series; closed administratively. - PMS-6 login rate-limit: already shipped via `src/modules/auth/rate_limit.rs`; closed administratively. - PMS-7 password reset email: new `utils::email::Mailer` (LogMailer dev, SmtpMailer prod via lettre). Selected by `SMTP_HOST` presence. Wired into `AuthService::request_password_reset`. Frontend link is built from `CLIENT_ORIGIN`. - PMS-8 welcome email on user create: mints a 7-day password-reset token and emails the new user a `/reset-password/<token>` link, reusing the existing reset machinery rather than introducing a parallel setup-token table. - PMS-9 MFA TOTP: real verification in the login flow against `users.mfa_secret` (base32) via `mokosh_auth_crypto::totp::verify` (+-1 step skew). Three new endpoints: `POST /me/mfa/setup` (stage secret + provisioning URI), `POST /me/mfa/enable` (confirm one code, flip flag), `POST /me/mfa/disable` (clear after password re-auth). - PMS-10 personal API keys: `POST /me/api-keys`, `GET /me/api-keys`, `DELETE /me/api-keys/:id`. Raw key returned once on create; DB stores only `key_prefix` (10-char index) + argon2 hash. ## Behaviour-visible changes - `AuthService::new` is preserved; new `AuthService::with_mailer` is the production constructor (called by `create_api_router`). - `create_api_router` gains a final `mailer: Arc<dyn Mailer>` parameter; `main.rs` builds it from `MailerConfig::from_env`. The boot path hard-fails on misconfiguration (e.g. SMTP_USERNAME without SMTP_PASSWORD) rather than silently downgrading to LogMailer. - `users.mfa_secret` is now read/written on enrollment and consulted on login when `mfa_enabled = true`. No schema change required. - `api_keys` rows are created on `POST /me/api-keys`; no bearer-auth middleware change yet (kept out of scope; tracked separately). ## Test plan - [ ] `cargo check --bin mokosh-server` clean (verified locally). - [ ] Smoke: `POST /api/v1/auth/forgot-password` for a known user logs the reset link (LogMailer dev) or sends via SMTP (compose.dev.yml mailpit at http://localhost:8025). - [ ] Smoke: `POST /api/v1/auth/me/mfa/setup` returns secret + URI; an authenticator app loaded with the URI produces codes that `POST /me/mfa/enable` accepts; subsequent login requires the code. - [ ] Smoke: `POST /api/v1/auth/me/api-keys` returns a `psa_...` key; `GET` lists with prefix only; `DELETE` removes it. Closes #PMS-4
Add `utils:📧:Mailer` with `LogMailer` (dev) and `SmtpMailer` (lettre). `MailerConfig::from_env` selects the implementation based on `SMTP_HOST`. `AuthService::request_password_reset` now dispatches the reset link through the configured mailer; the link is built from the new `frontend_base_url` field (sourced from `CLIENT_ORIGIN`).

The mailer is constructed once at startup and threaded through `create_api_router` -> `AuthService::with_mailer`. Send failures are logged but do not change the public response shape, preserving the existing enumeration-resistant behaviour of `/auth/forgot-password`.

#PMS-7 State Done
When `create_user` is called with `send_welcome_email = true`, mint a fresh entry in `password_reset_tokens` (7-day TTL) and email the new user a setup link. The recipient lands on `/reset-password/<token>`, picks a password, and can sign in. Reuses the password-reset flow rather than introducing a parallel `account_setup_tokens` table.

#PMS-8 State Done
Wire RFC 6238 TOTP verification into the password login flow: when `users.mfa_enabled` is true and the request carries an `mfa_code`, decode `users.mfa_secret` (base32) and validate the code against `mokosh_auth_crypto::totp::verify` with +-1 step tolerance. Codes that fail produce `Unauthorized` rather than the prior silent-acceptance behaviour.

Three new endpoints round out enrollment: `POST /me/mfa/setup` stages a fresh secret + provisioning URI (the frontend turns the URI into a QR code), `POST /me/mfa/enable` confirms one code before flipping `mfa_enabled`, and `POST /me/mfa/disable` clears the flag after re-authenticating with the user's password. Partial enrollment (secret persisted, flag still false) protects users from getting locked out by a misconfigured authenticator.

#PMS-9 State Done
feat(auth): personal API key CRUD (issue / list / revoke)
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
e90708c53a
The `api_keys` table has lived in the initial migration since day one but had no handlers. Wire `POST /me/api-keys` (mint), `GET /me/api-keys` (list, no secret material), and `DELETE /me/api-keys/:id` (revoke; hard-delete since the row carries no audit value once gone).

The raw `psa_<40 alnum>` bearer key is returned exactly once in the create response. The DB stores only `key_prefix` (first 10 chars, indexed) and an argon2 hash of the rest; future bearer-auth middleware will look up by prefix in O(log n) and confirm with `verify_password`. Revocation is scoped to `(tenant_id, user_id)` so a session for user A cannot kill user B's keys.

#PMS-10 State Done
vas2000-work closed this pull request 2026-05-21 02:42:53 +02:00
Some checks are pending
Create release / Create release from merged PR (pull_request) Has been skipped
Check / * (pull_request)
Required
E2E / * (pull_request)
Required

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!20
No description provided.