feat(auth): verify email addresses before login #15

Merged
ryan.mcgee merged 39 commits from feat/email-verification into main 2026-10-15 18:08:21 +00:00
Owner

Summary

  • With an SMTP server configured, people cannot log in until they confirm their email address.
  • Stacked on #14 (feat/oidc); second of three auth changes (OIDC, email verification, then 2FA / WebAuthn).

Changes

  • TRAEFIK_CONTROL_EMAIL_VERIFICATION = required or off; defaults to required when SMTP is configured, otherwise off; required without SMTP refuses to start (all six combinations exercised).
  • Every sign-in path (password, registration, invitation, OIDC) ends in UserAuth.log_in_user, which now makes no session for an unconfirmed address and mails a link instead. Existing sessions of unconfirmed users stop working (Accounts.get_user_by_session_token).
  • The mail goes out only after valid credentials, at most one a minute per user, so the login form cannot be used to mail strangers or to tell whether an address exists.
  • Links are single use, expire after 3 days, and only work while the address is still the one they were sent to. Only the SHA-256 is stored. The link opens a page with a Confirm button, so mail scanners and link previews cannot use the token up.
  • Confirmed from the start: the first account of an instance, accounts created by an identity provider (which verified the email), and (by migration backfill) every existing user. Invited and self-registered accounts are not.
  • TraefikControl.Mail extracted from the alert delivery code (sender parsing, sending, configured?); Alerts.email_configured? is replaced by Mail.configured?.
  • README updated.

Verification

  • mix format --check-formatted, mix compile --warnings-as-errors, mix credo --strict, mix dialyzer: clean.
  • mix test: 1204 passed on SQLite and on PostgreSQL (confirmation context, web flows including OIDC and registration, throttle, expiry, wrong-address, single use, crashing and refusing mail adapters).
  • Migration round trip with data on both backends: existing users backfilled as confirmed, new ones not; rollback and re-apply.
  • Smoke test with Mailpit over real SMTP and a browser: registered a second account, got the mail (sender name and address correct), a repeat login was throttled without a second mail, the link opened the Confirm page, confirming allowed the login, and reusing the link was refused.
## Summary - With an SMTP server configured, people cannot log in until they confirm their email address. - Stacked on #14 (`feat/oidc`); second of three auth changes (OIDC, email verification, then 2FA / WebAuthn). ## Changes - `TRAEFIK_CONTROL_EMAIL_VERIFICATION` = `required` or `off`; defaults to `required` when SMTP is configured, otherwise `off`; `required` without SMTP refuses to start (all six combinations exercised). - Every sign-in path (password, registration, invitation, OIDC) ends in `UserAuth.log_in_user`, which now makes no session for an unconfirmed address and mails a link instead. Existing sessions of unconfirmed users stop working (`Accounts.get_user_by_session_token`). - The mail goes out only after valid credentials, at most one a minute per user, so the login form cannot be used to mail strangers or to tell whether an address exists. - Links are single use, expire after 3 days, and only work while the address is still the one they were sent to. Only the SHA-256 is stored. The link opens a page with a Confirm button, so mail scanners and link previews cannot use the token up. - Confirmed from the start: the first account of an instance, accounts created by an identity provider (which verified the email), and (by migration backfill) every existing user. Invited and self-registered accounts are not. - `TraefikControl.Mail` extracted from the alert delivery code (sender parsing, sending, `configured?`); `Alerts.email_configured?` is replaced by `Mail.configured?`. - README updated. ## Verification - `mix format --check-formatted`, `mix compile --warnings-as-errors`, `mix credo --strict`, `mix dialyzer`: clean. - `mix test`: 1204 passed on SQLite and on PostgreSQL (confirmation context, web flows including OIDC and registration, throttle, expiry, wrong-address, single use, crashing and refusing mail adapters). - Migration round trip with data on both backends: existing users backfilled as confirmed, new ones not; rollback and re-apply. - Smoke test with Mailpit over real SMTP and a browser: registered a second account, got the mail (sender name and address correct), a repeat login was throttled without a second mail, the link opened the Confirm page, confirming allowed the login, and reusing the link was refused.
- move sender parsing and delivery out of the alert channel code so
  other features can send email
- Mail.configured? replaces Alerts.email_configured?
- with SMTP configured, log in requires a confirmed address
  (TRAEFIK_CONTROL_EMAIL_VERIFICATION, required by default then)
- every sign-in path goes through UserAuth.log_in_user, which mails a
  link instead of making a session; sessions of unconfirmed users end
- links are single use, expire after 3 days, bind to the address they
  were sent to, and are confirmed by a button so scanners cannot use
  them up; at most one mail a minute, only after valid credentials
- first account, provider-verified accounts and existing users are
  confirmed; migration backfills confirmed_at
ryan.mcgee changed target branch from feat/oidc to main 2026-10-15 18:08:11 +00:00
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
White_Ghost_Cyber_Systems/traefik-control!15
No description provided.