# Android Cast — Email agent config & 2FA / mobile auth flow

_Date: 2026-06-07 — FR §9 (email) + FR §10 (registration, 2FA, mobile) — **alpha must-have**_

---

---

---

---

---

## Table of contents

<!-- toc -->
- [Document history](#document-history)
- [Executive summary](#executive-summary)
- [Current state (repo)](#current-state-repo)
- [§9 — Email addresses & agent config](#9-email-addresses-agent-config)
  - [§9.1 Prerequisites](#91-prerequisites)
  - [§9.2 Register addresses (free — DNS, not “buy mailboxes”)](#92-register-addresses-free-dns-not-buy-mailboxes)
  - [§9.3 Forward / duplicate to Gmail (info@ → foxxspambox0@gmail.com)](#93-forward-duplicate-to-gmail-info-foxxspambox0gmailcom)
  - [§9.4 Recommended prod layout for this project](#94-recommended-prod-layout-for-this-project)
  - [§9.5 Outbound mail from BE (verification emails)](#95-outbound-mail-from-be-verification-emails)
- [§10 — Registration, 2FA, mobile auth](#10-registration-2fa-mobile-auth)
  - [§10.1 Registration & login stages](#101-registration-login-stages)
  - [§10.2 Open-source building blocks (recommended)](#102-open-source-building-blocks-recommended)
  - [§10.3 Users DB migration (privacy-first)](#103-users-db-migration-privacy-first)
  - [§10.4 On-demand user maintenance (admin + self-service)](#104-on-demand-user-maintenance-admin-self-service)
  - [§10.5 Operator checklist (track progress)](#105-operator-checklist-track-progress)
  - [§10.6 Mobile-friendly UX (not BE-only)](#106-mobile-friendly-ux-not-be-only)
  - [§10.7 Alpha implementation plan](#107-alpha-implementation-plan)
- [UI/UX alignment (existing consoles)](#uiux-alignment-existing-consoles)
- [Security & compliance notes](#security-compliance-notes)
- [Open questions](#open-questions)
- [Source linkage](#source-linkage)
<!-- /toc -->

**Documentation index:** [README.md](README.md)

---

## Document history

| Rev | Date | Focus |
|-----|------|--------|
| **1** | 2026-06-07 | Initial FR capture: email forwarding + registration/2FA for alpha; infra + BE + mobile flows. |

**Alpha gate:** agreed with project owners — user registration with verified email + second factor is **must-have for full alpha** (alongside RSSH remote access). LAN-only cast demo may still skip account flows.

---

## Executive summary

| Track | Goal |
|-------|------|
| **§9 Email** | Public-facing addresses `info@`, `admin@`, `root@` on **`apps.f0xx.org`** (and optional aliases on **`f0xx.org`**) that **forward** to personal Gmail; no paid mailbox SKU required. |
| **§10 Auth** | Replace “Register (coming soon)” with **email verification + flexible 2FA** (TOTP QR, optional WebAuthn/passkey on Android), captcha on abuse-prone endpoints, **privacy-preserving** audit tables, admin tools for root/slug admins. |
| **Mobile** | Same flows usable on phone browsers **and** deep links / Custom Tabs from the Android app where appropriate. |
| **Licenses** | Prefer **MIT / BSD / Apache-2.0** PHP libraries; avoid proprietary-only auth SaaS for core path. |

---

## Current state (repo)

| Area | Today |
|------|--------|
| BE login | Username + `password_hash` only — [`Auth.php`](../examples/crash_reporter/backend/src/Auth.php) |
| Registration | UI placeholder: “Register (coming soon)” — [`login.php`](../examples/crash_reporter/backend/views/login.php) |
| Users table | `username`, `password_hash`, `role` — no email, no 2FA — [`schema.mariadb.sql`](../examples/crash_reporter/backend/sql/schema.mariadb.sql) |
| Outbound email | **Not configured** — no SMTP in [`config.example.php`](../examples/crash_reporter/backend/config/config.example.php) |
| Mobile app | No account linking to crashes console yet |
| Roadmap | “User registration + email verification” — [`examples/crash_reporter/ROADMAP.md`](../examples/crash_reporter/ROADMAP.md) |

---

## §9 — Email addresses & agent config

Target addresses (any one canonical pair is fine; document both for flexibility):

| Role | Preferred | Alias |
|------|-----------|--------|
| Public / info | `info@apps.f0xx.org` | `info@f0xx.org` |
| Admin / ops | `admin@apps.f0xx.org` | `admin@f0xx.org`, `root@apps.f0xx.org` |

**Agent config** (future): BE `config.php` → `mail.from`, `mail.reply_to`, `mail.envelope_from` pointing at these addresses once DNS and forwarding work.

### §9.1 Prerequisites

You do **not** need to buy Google Workspace or Microsoft 365 mailboxes. You **do** need:

| # | Prerequisite | Why |
|---|--------------|-----|
| 1 | **DNS control** for `f0xx.org` and `apps.f0xx.org` | MX, SPF, DKIM, DMARC live at the DNS host (registrar or Cloudflare). |
| 2 | **Decide canonical mail domain** | Recommend **`apps.f0xx.org`** for product mail; keep `f0xx.org` as forward/alias only if desired. |
| 3 | **Inbound path** | MX records → free forwarder **or** self-hosted Postfix alias on FE/BE. |
| 4 | **Outbound path (BE)** | SMTP relay for verification/reset mail (see §9.5) — can be Gmail SMTP, SendGrid free tier, or local Postfix with SPF alignment. |
| 5 | **TLS on web** | Already have `https://apps.f0xx.org` — verification links use same vhost. |
| 6 | **Gmail destination** | e.g. `foxxspambox0@gmail.com` — receives forwarded + BCC copies. |

**Deliverability minimum (avoid spam folder):**

```text
SPF   TXT @apps.f0xx.org   "v=spf1 include:<forwarder-or-smtp-provider> ~all"
DKIM  TXT (provider gives name/value)
DMARC TXT _dmarc.apps.f0xx.org  "v=DMARC1; p=none; rua=mailto:admin@apps.f0xx.org"
```

Start with `p=none`; tighten after monitoring.

### §9.2 Register addresses (free — DNS, not “buy mailboxes”)

**“Register” here = DNS + forwarder rules**, not purchasing seats.

#### Option A — **Cloudflare Email Routing** (recommended if DNS is on Cloudflare)

1. Cloudflare dashboard → **Email** → **Email Routing** → enable for `apps.f0xx.org`.
2. Add **destination address** `foxxspambox0@gmail.com` (verify Gmail link once).
3. Create **Custom addresses**:
   - `info@apps.f0xx.org` → forward to Gmail
   - `admin@apps.f0xx.org` → forward to Gmail
   - `root@apps.f0xx.org` → forward to Gmail (or same rule as admin)
4. Cloudflare adds MX automatically; copy **SPF** hint from dashboard.
5. Repeat for **`f0xx.org`** zone if you want `info@f0xx.org` (separate zone rules or redirect-only).

**Cost:** $0 on Cloudflare free plan.

#### Option B — **ImprovMX** (DNS at any registrar)

1. Sign up at [improvmx.com](https://improvmx.com) (free tier: limited aliases).
2. Add domain `apps.f0xx.org` → set MX to ImprovMX hosts (they show exact records).
3. Create aliases `info`, `admin`, `root` → `foxxspambox0@gmail.com`.
4. Add SPF include for ImprovMX.

#### Option C — **Self-hosted alias** (Gentoo FE or Alpine BE)

If you already run Postfix on FE:

```text
/etc/postfix/virtual:
info@apps.f0xx.org    foxxspambox0@gmail.com
admin@apps.f0xx.org   foxxspambox0@gmail.com
root@apps.f0xx.org    foxxspambox0@gmail.com
```

Then `postmap /etc/postfix/virtual && systemctl reload postfix`. Requires **MX** pointing to FE public IP and port **25** reachable (often blocked on residential; OK on HVM with firewall rule).

**Do first:** pick **A or B** unless you explicitly want to operate SMTP on FE.

### §9.3 Forward / duplicate to Gmail (info@ → foxxspambox0@gmail.com)

| Method | Inbox copy | Send-as From | IMAP on apps domain |
|--------|------------|--------------|---------------------|
| **Forward only** (Cloudflare / ImprovMX) | Yes — same subject/body in Gmail | Reply-from Gmail unless configured | N/A — read in Gmail |
| **Gmail “Send mail as”** | N/A | Can send as `info@apps.f0xx.org` if SMTP creds exist | Optional |
| **BCC archive** | Forward + hidden copy to second Gmail | — | Rarely needed |

For FR §9.3 (“same topic and body redirected”): **plain forward** is enough. Gmail shows original `To:` header; you can filter with Gmail rule `to:info@apps.f0xx.org`.

**Optional Gmail filter:**

```text
To: info@apps.f0xx.org  →  label: androidcast-info
```

No IMAP/POP3 server on `apps.f0xx.org` is required for this FR.

### §9.4 Recommended prod layout for this project

```text
Inbound (public):
  info@apps.f0xx.org   ──forward──► foxxspambox0@gmail.com
  admin@apps.f0xx.org  ──forward──► foxxspambox0@gmail.com
  root@apps.f0xx.org   ──forward──► foxxspambox0@gmail.com (same inbox or +tag)

Outbound (BE PHP):
  From: Android Cast Issues <noreply@apps.f0xx.org>
  Reply-To: info@apps.f0xx.org
  SMTP: transactional relay (see §9.5)
```

Document chosen provider in [`INFRA.md`](INFRA.md) § changelog once live.

### §9.5 Outbound mail from BE (verification emails)

Add to `config.example.php` (implementation phase):

```php
'mail' => [
    'transport' => 'smtp', // smtp | sendmail
    'from' => 'Android Cast Issues <noreply@apps.f0xx.org>',
    'reply_to' => 'info@apps.f0xx.org',
    'smtp' => [
        'host' => 'smtp.example.com',
        'port' => 587,
        'encryption' => 'tls',
        'username' => '',
        'password' => '', // env / secrets file, not git
    ],
],
```

**Free/low-cost SMTP options:**

| Provider | Notes |
|----------|--------|
| Gmail SMTP + App Password | OK for low volume; From may need “Send as” alignment |
| Brevo / SendGrid free tier | Better for `noreply@apps.f0xx.org` alignment |
| Local Postfix on BE | Full control; must publish SPF for BE egress IP |

PHP library: **Symfony Mailer** (MIT) — already common in PHP ecosystems.

---

## §10 — Registration, 2FA, mobile auth

### §10.1 Registration & login stages

Proposed **alpha** flow (flexible — toggles in config):

```mermaid
sequenceDiagram
    participant U as User (mobile browser)
    participant BE as apps.f0xx.org PHP
    participant M as Mail / Gmail
    participant A as Android app (optional)

    U->>BE: POST /register (email, username, password)
    BE->>BE: captcha verify, rate limit
    BE->>BE: store pending registration (hashed secrets)
    BE->>M: Send verify link (24h token)
    M->>U: Email with https://apps.f0xx.org/.../verify?token=
    U->>BE: GET verify (or Custom Tab from app)
    BE->>U: Prompt enroll 2FA (TOTP QR and/or WebAuthn)
    U->>BE: POST /2fa/enroll (TOTP secret or passkey)
    BE->>U: Account active → login

    Note over U,BE: Login thereafter
    U->>BE: username + password
    BE->>U: 2FA challenge (TOTP code or WebAuthn)
    U->>BE: verify second factor
    BE->>U: Session cookie (existing ac_crash_sess)
```

| Stage | Method | Mobile-friendly |
|-------|--------|-----------------|
| **10.1.2 Email confirmation** | HTTPS link with signed token | Email app → Chrome Custom Tab; large tap target |
| **10.1.3 TOTP / QR** | Standard `otpauth://` QR + manual key | Scan with Google Authenticator / Aegis; or type 6 digits |
| **10.1.4 WebAuthn / passkey** | Platform authenticator (fingerprint) | **Android Chrome** + future app WebView if needed |
| **Captcha** | ALTCHA (MIT, self-hosted) or rate limit only at alpha | Invisible/PoW — no Google reCAPTCHA dependency |
| **Recovery** | Backup codes + optional second recovery email | Printable one-time codes; store hashed |

**Roles:** new self-registered users default to `viewer` + default company membership (existing RBAC). Elevation to `admin`/`root` remains **admin-only**.

### §10.2 Open-source building blocks (recommended)

| Function | Library | License | Notes |
|----------|---------|---------|-------|
| SMTP | [symfony/mailer](https://github.com/symfony/mailer) | MIT | Transport abstraction |
| TOTP | [spomky-labs/otphp](https://github.com/spomky-labs/otphp) | MIT | RFC 6238; QR via endroid/qr-code |
| WebAuthn | [web-auth/webauthn-lib](https://github.com/web-auth/webauthn-lib) | MIT | Passkeys; PHP 8.1+ |
| Password hash | `password_hash()` | PHP core | Already used |
| Captcha | [altcha-org/altcha](https://github.com/altcha-org/altcha) | MIT | Self-hosted PoW widget |
| JWT / tokens | Signed random + HMAC in DB | — | Prefer opaque tokens stored hashed |

**Avoid as hard dependency for alpha:** proprietary Auth0/Okta-only flows (OK as optional future SSO).

**Android app (phase 2 of alpha auth):**

| Feature | Approach |
|---------|----------|
| Open verify link | `CustomTabsIntent` → `https://apps.f0xx.org/.../verify` |
| TOTP | Optional in-app entry (Web login sufficient for alpha) |
| Passkey | Chrome on device; link account in settings later |
| Deep link | `androidcast://auth/verify?token=` optional — not required if HTTPS works |

### §10.3 Users DB migration (privacy-first)

New migration **`008_auth_email_2fa.sql`** (sketch — apply on MariaDB prod once implemented):

**Extend `users`:**

| Column | Type | Notes |
|--------|------|-------|
| `email_normalized` | VARCHAR(254) NULL UNIQUE | Lowercase email |
| `email_verified_at` | TIMESTAMP NULL | NULL until link clicked |
| `status` | ENUM | `pending`, `active`, `locked`, `disabled` |
| `recovery_email_normalized` | VARCHAR(254) NULL | Optional second address |

**New tables (anonymous / hashed where possible):**

```sql
-- auth_pending_registrations: delete after verify or TTL
-- auth_factors: user_id, type ENUM('totp','webauthn','backup_code'), secret_or_credential_encrypted, label, created_at
-- auth_tokens: purpose ENUM('verify_email','reset_password','login_magic'), token_hash, expires_at, used_at
-- auth_attempts: ip_hash, username_hash, outcome, created_at  (no raw IP in prod if avoidable)
-- auth_audit: actor_user_id, action, meta_json, created_at
```

**Privacy rules:**

- Store **hashed** opaque tokens (SHA-256), never raw token in DB.
- Store TOTP secrets **encrypted at rest** (libsodium `secretbox`, key in `config.php` env).
- `auth_attempts.ip_hash` = HMAC(server_pepper, ip) — supports rate limit without retaining IP.
- WebAuthn credentials: store `credential_id` + `public_key` only (standard).

SQLite dev: mirror schema in `Database::ensureSchema` like existing RBAC migrations.

### §10.4 On-demand user maintenance (admin + self-service)

| Action | Who | UI location |
|--------|-----|-------------|
| Add / change **recovery email** | User | `?view=account_security` (new) |
| Re-enroll TOTP / add passkey | User | Same; requires recent password + 2FA |
| Revoke passkeys / backup codes | User | Same |
| Clear failed login/register attempts | `root`, company `admin` | `?view=rbac` → user row → “Clear auth lockouts” |
| Disable user / force password reset | `root`, `platform_admin` | RBAC admin |
| View auth audit tail | `root` | `?view=auth_audit` (read-only) |

Session cookie path stays **`/app/androidcast_project`** (shared across crashes/build consoles).

### §10.5 Operator checklist (track progress)

Use this table during implementation (copy to ticket **#9 / #10**):

| # | Task | Owner | Done |
|---|------|-------|------|
| E1 | Enable Cloudflare Email Routing (or ImprovMX) for `apps.f0xx.org` | Ops | ☐ |
| E2 | Forward `info@`, `admin@`, `root@` → Gmail | Ops | ☐ |
| E3 | SPF + DKIM + DMARC records live | Ops | ☐ |
| E4 | Test inbound: send mail to `info@apps.f0xx.org`, arrives Gmail | Ops | ☐ |
| E5 | Configure BE SMTP + send test from staging | Dev | ☐ |
| A1 | Migration `008_auth_email_2fa.sql` + `Database::ensureSchema` | Dev | ☐ |
| A2 | Register + verify email views (EN/RU, `app.css`) | Dev | ☐ |
| A3 | TOTP enroll + login challenge | Dev | ☐ |
| A4 | WebAuthn enroll (optional alpha) | Dev | ☐ |
| A5 | ALTCHA on register/login | Dev | ☐ |
| A6 | Admin: clear lockouts + audit view | Dev | ☐ |
| A7 | Mobile soak: register on phone Chrome, verify, login | QA | ☐ |
| A8 | Document secrets in INFRA + config.example.php | Dev | ☐ |

### §10.6 Mobile-friendly UX (not BE-only)

| Requirement | Implementation |
|-------------|----------------|
| Viewport | Already `width=device-width` in [`layout.php`](../examples/crash_reporter/backend/views/layout.php) |
| Touch targets | Buttons ≥ 44px; reuse `.login-card`, `.btn` from [`app.css`](../examples/crash_reporter/backend/public/assets/css/app.css) |
| i18n | Extend [`en.json` / `ru.json`](../examples/crash_reporter/backend/public/assets/i18n/) |
| Email links | Single primary button “Confirm email” — no raw URL only |
| 2FA QR | Responsive `<img>` or SVG; “Can’t scan?” reveals manual secret |
| Errors | Same `.alert` pattern as login |
| Android app | Settings → “Project account” → opens Custom Tab to account/security (post-alpha polish OK) |

### §10.7 Alpha implementation plan

| Phase | Scope | Estimate |
|-------|--------|----------|
| **P0 — Email infra** | §9 DNS + forward + outbound SMTP test | 0.5–1 day ops |
| **P1 — Register + verify** | Pending table, mailer, verify link, captcha | 2–3 days |
| **P2 — TOTP login** | Enroll QR + challenge on login | 1–2 days |
| **P3 — Admin tools** | Recovery email, clear lockouts, audit | 1 day |
| **P4 — WebAuthn** | Passkey enroll/login (Chrome/Android) | 1–2 days optional for alpha |
| **P5 — Mobile app link** | Custom Tab entry points | 0.5 day |

**Alpha sign-off (auth):** new user can register on phone, verify email, enroll TOTP, log in with 2FA; admin can clear lockout; inbound `info@` mail reaches Gmail.

Update [`ALPHA.md`](ALPHA.md) § G (auth) and [`ROADMAP.md`](ROADMAP.md) alpha backend checklist when P1 starts.

---

## UI/UX alignment (existing consoles)

Do **not** introduce a new design system. Reuse:

| Asset | Path |
|-------|------|
| Styles | `public/assets/css/app.css` — `.login-page`, `.login-card`, `.alert`, `.muted`, `.locale-picker` |
| Layout shell | `views/layout.php` — nav, hub card styling |
| i18n | `public/assets/js/i18n.js` + JSON catalogs |
| Analytics hook | `AnalyticsHead::render()` on auth pages (optional) |

New views (planned):

- `views/register.php`
- `views/verify_email.php`
- `views/account_security.php`
- `views/two_factor_challenge.php`

Hub landing contact form (`landing-pages.inc.php`) should eventually use **`info@apps.f0xx.org`** as `mailto:` — align when email live.

---

## Security & compliance notes

- Enforce **rate limits** on register, login, verify resend (e.g. 5/min/IP hash).
- Lock account after N failed 2FA attempts; unlock via email or admin.
- Do **not** log passwords or TOTP codes.
- Backup codes: generate 10 one-time codes, display once, store bcrypt hashes.
- GDPR-style minimization: optional `auth_attempts` retention job (purge > 90 days).

---

## Open questions

| # | Question | Default proposal |
|---|----------|------------------|
| Q1 | Require WebAuthn for alpha or TOTP-only? | **TOTP required**; WebAuthn optional P4 |
| Q2 | Allow registration on prod immediately or invite-only? | **Open register** with captcha; `viewer` role |
| Q3 | Single Gmail inbox for all aliases? | Yes for now (`foxxspambox0@gmail.com`) |
| Q4 | Separate `noreply@` for outbound? | Yes — no forward needed; SPF only |

---

## Source linkage

| Topic | Doc / code |
|-------|------------|
| Infra / DNS | [INFRA.md](INFRA.md) |
| Crash console | [CRASH_REPORTER.md](CRASH_REPORTER.md), [backend README](../examples/crash_reporter/backend/README.md) |
| RBAC | [backend README § RBAC](../examples/crash_reporter/backend/README.md#rbac-phase-1--admin-ui) |
| Alpha gates | [ALPHA.md](ALPHA.md), [ROADMAP.md](ROADMAP.md) |
| PDF build | [`scripts/build-all-docs-pdf.sh`](../scripts/build-all-docs-pdf.sh) |
