Skip to content

AUTH-MAIL-002C2 — Handle verification links safely without exposing action codes #194

Closed

Description

@daliu

Officer impact: Members get one plain verification-link page with a deliberate Verify email action and safe recovery wording. Officers never ask for, copy, or open a member's action link or code.

Officer documentation: Update docs/officers/EMERGENCY_AND_RECOVERY.md, docs/officers/EVENTS_SHOP_MEMBERS.md, and docs/officers/SYSTEM_MAPS.md. Keep the route SOURCE ONLY, NOT LIVE until exact website, Firebase template, sender, and synthetic staged evidence exist.

Deployment evidence: None at creation. This child proves source/tests/merge only. It does not configure Firebase templates, sender/domain/DNS, publish a website, or test a production mailbox/action link.

Parent, dependency, and exact claim

Focused AUTH-MAIL-002C2 child of #120. #99, #103, and #111 provide callback preservation, deterministic no-network tests, and diagnostic redaction. #145/#153/#155 are merged. #178 released its separate My Account phone/profile paths.

CLAIMED by /root at 2026-07-13T23:38:15Z on codex/issue-194-verify-email-action, based on exact main aafd15c04fbcd09341092a78f23d513b09cca5d2.

Owned paths only:

  • src/pages/login/VerifyEmailAction.tsx (new)
  • src/pages/login/VerifyEmailAction.test.tsx (new)
  • src/pages/login/VerifyEmailAction.css (new)
  • src/services/identity/Identity.ts
  • src/services/identity/Identity.test.ts
  • src/App.jsx
  • src/App.test.jsx
  • src/services/monitoring/capabilityCallback.ts
  • src/services/monitoring/sentry.test.ts
  • src/services/firebase/FirebaseResources.test.ts
  • tests/spa-navigation.test.js
  • docs/officers/EMERGENCY_AND_RECOVERY.md
  • docs/officers/EVENTS_SHOP_MEMBERS.md
  • docs/officers/SYSTEM_MAPS.md
  • minimum exact C2 status hunks in SECURITY.md, OPERATIONS_RUNBOOK.md, GITHUB_ISSUES.md, and GITHUB_ISSUE_SLICES.md

Excluded: Account/profile/phone paths, Functions, Rules, packages, workflows, Firebase/Netlify/DNS/template/provider settings, password-entry/reset completion, recover-email mutation, Google login, roles/membership, Firestore data, and every other issue.

Problem

There is no website route for incoming Firebase Auth action links. A proposed one-time code in the query would currently fall through to 404 while Sentry and App Check/reCAPTCHA can initialize before React removes it. Automatically applying a code on page load is also unsafe because mail-security scanners can open links.

Firebase's official custom-handler design uses one handler for verifyEmail, resetPassword, and recoverEmail. This child is intentionally verification-only source. Do not point Firebase's project-wide custom action URL at this partial route. #119 must keep the default multi-mode handler or wait for separately reviewed coverage of every enabled mode.

Official design reference: https://firebase.google.com/docs/auth/custom-email-handler

Atomic outcome

Add a public /auth/action source route that:

  1. Captures exactly one bounded verification code into component memory after the hosting handoff. The existing SAFETY-001 — Preserve callbacks and fail closed on local Firebase isolation #99 Pages fallback may briefly use tab-local session storage as clarified below.
  2. Removes all query and fragment text from visible history before any provider call.
  3. Starts no Sentry/App Check outside client while the capability is present.
  4. Waits for one deliberate native Verify email activation; page load never mutates an account.
  5. Calls checkActionCode, requires provider operation VERIFY_EMAIL, and only then calls applyActionCode.
  6. Returns only small public states: ready, working, verified, already complete when provable, different signed-in account when provable, unusable, or temporarily unavailable.
  7. Uses one fixed same-origin recovery action and never follows query apiKey or continueUrl.

Security and usability invariants

  • Query mode is untrusted; provider-returned operation authorizes the exact action.
  • Duplicate, blank, malformed, unsupported, control-character, or oversized parameters make no provider call.
  • No email, code, query, fragment, action URL, provider error, UID, or raw metadata enters DOM, title/meta, logs, diagnostics, analytics, or screenshots. After the existing SAFETY-001 — Preserve callbacks and fail closed on local Firebase isolation #99 Pages restore reads and deletes its tab-local handoff, AUTH-MAIL-002C2 — Handle verification links safely without exposing action codes #194 writes no capability to storage and removes it from native history and React Router state before enabling the action. A failed Pages root load can leave the already-merged handoff value until tab close; direct-rewrite hosting avoids that residual.
  • Invalid, expired, used, disabled, deleted, wrong-environment, malformed, and wrong-operation links share one honest result. Firebase cannot reliably distinguish all of them.
  • Offline, throttled, network, and unknown provider failures share one temporary result and a deliberate retry.
  • A different signed-in account blocks apply without rendering either address.
  • Verification changes Firebase Auth email verification only. It never grants membership/admin, changes claims, refreshes roles, writes Firestore, calls Functions, creates/signs in an account, or sends mail.
  • A synchronous in-flight guard prevents double activation. Late completion after unmount cannot update the page.
  • The action is keyboard-operable, visibly focused, at least 48px high, and exposes busy/disabled state. Final status receives focus with polite atomic announcement.
  • Tests use synthetic codes/mocks only and make no network or production request.

Acceptance criteria

  • /auth/action renders the handler rather than 404 and Pages deep-link restoration preserves the initial query only long enough for immediate scrub.
  • Search/hash and navigation state are removed from native history and React Router before the action is enabled or an action-code promise starts; after the existing SAFETY-001 — Preserve callbacks and fail closed on local Firebase isolation #99 Pages handoff is read and deleted, AUTH-MAIL-002C2 — Handle verification links safely without exposing action codes #194 retains the code only in its component ref.
  • No checkActionCode, applyActionCode, or account mutation occurs on page load, rerender, or mail-scanner-style visit without activation. Normal Firebase Auth client/session initialization is not represented as an action-code call.
  • Exactly one valid VERIFY_EMAIL check leads to exactly one apply; rapid repeat activation does not duplicate either call.
  • A provider operation mismatch, wrong signed-in account, or malformed/duplicate/oversized request never applies the code.
  • Success, already-complete, unusable, and temporary results are truthful, fixed, accessible, and contain no account/token/provider detail.
  • Hostile apiKey, continueUrl, absolute/protocol-relative URL, fragment, email, token, and provider-error canaries cannot affect authority/navigation or appear in output/logs.
  • Sentry and App Check stay off for case, trailing-slash, encoded, query, and fragment forms of the verification callback.
  • Retry, check/apply race, wrong-account, unmount, focus, keyboard, busy, disabled, and 48px target tests pass without network.
  • Focused/full frontend, SPA, lint baseline, build, Functions, Rules, commerce-emulator, artifact/workflow, diff/link, and sensitive-pattern checks pass.
  • Officer/system maps explain the deliberate-click path, safe stop conditions, proof, undo, escalation, and NOT LIVE provider boundary.
  • Independent security/test/officer reviews approve exact diff; merge only after exact-head hosted checks.

Migration, rollback, and live stop condition

No data migration. Roll back the route/service/guard/docs together if focused tests expose a regression. The route stays source-only unless #119 privately proves a safe provider design. Stop any provider cutover if the configured action URL would send resetPassword or recoverEmail to this verification-only handler.

Activity

  1. self-assigned this
    on Jul 13, 2026
  2. added
    bugSomething isn't working
    priority:P1High-priority follow-up
    type:securitySecurity or privacy boundary
    area:authAuthentication and authorization
    area:firebaseFirebase services and data
    area:webWeb application and hosting
    size:MMedium multi-file issue
    status:in-progressClaimed and actively being implemented
    type:testingTest infrastructure and quality gates
    on Jul 13, 2026
  3. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    PRE-EDIT ROUTE REFINEMENT — 2026-07-13T23:41:00Z

    Independent provider review confirmed Firebase configures one generic action-handler URL for all Auth email modes. Before any source edit, the claimed route changes from /account/verify-email to generic /auth/action. This C2 dispatcher still accepts only exact mode=verifyEmail plus provider-returned VERIFY_EMAIL; every other mode fails closed with the fixed unusable result.

    The generic path can be extended by future separately claimed reset/recover slices without changing provider routing. It does not make the partial handler safe to enable globally now. All owned files and exclusions remain unchanged; the issue body now uses the corrected route.

  4. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    SCOPE AMENDMENT — 2026-07-13T23:43:30Z

    Failing-test review found two necessary owned paths before editing them:

    • src/services/firebase/FirebaseResources.ts: capture the initial capability-bearing location at module load. The new page scrubs the URL in a layout effect before the provider's later initialization effect; checking only the then-current URL could otherwise start App Check/reCAPTCHA after the scrub and lose the initial callback safety signal.
    • SYSTEM_DESIGN.md: add the minimum architecture truth for the bearer-capability route and project-wide Firebase handler boundary.

    This does not add Analytics, provider configuration, or another action mode. Sentry remains guarded synchronously; FirebaseResources only preserves the same shared guard decision through startup. All other ownership/exclusions remain unchanged.

  5. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    SCOPE AMENDMENT — FRONTEND LINT RECORD — 2026-07-13T23:49:00Z

    The merged non-mutating lint gate deliberately fingerprints every scanned frontend file as well as every finding. Adding the two claimed clean TS/TSX source/test files makes the gate reject even with zero new lint findings; Identity/App line shifts may also move existing recorded findings.

    Before editing it, #194 adds exact ownership of .github/lint-baseline.json for one audited mechanical update. Acceptance is strict: scanned files 102→104, configured findings remain exactly 125 errors / 7 warnings, every finding change must be only an understood line/column move caused by this issue, focused ESLint must add zero findings, and lint:ci must pass. The checker/workflow source, lint rules, severities, messages, and existing debt are unchanged. This is not permission to regenerate the record merely to make CI green.

  6. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    SCOPE AMENDMENT — 2026-07-13T23:51:43Z

    The exact repository workflow guard tests/ci-workflow.test.js is added to #194 ownership for one mechanical expectation update only: the complete frontend lint surface grows from 102 to 104 files because #194 adds VerifyEmailAction.tsx and its test. The expected lint errors (125) and warnings (7) do not change. No CI workflow, lint implementation, or acceptance rule changes.

    This was discovered by the required full workflow safety suite before staging.

  7. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    ACCEPTANCE CLARIFICATION — 2026-07-14T00:05:57Z

    Independent exact-diff review found two overbroad phrases in the issue text. This comment narrows them without weakening the mutation boundary:

    • “No provider call on page load” means no checkActionCode, applyActionCode, or account mutation before deliberate activation. The application still initializes its normal Firebase Auth client and persisted-session listener on startup.
    • “Component memory only” applies after hosting hands the route to React. The already-merged SAFETY-001 — Preserve callbacks and fail closed on local Firebase isolation #99 GitHub Pages fallback briefly stores the complete return route in tab-local sessionStorage, then the first root-page script reads and deletes it before React. If the root page never loads, that value can remain until the tab closes. Direct-rewrite hosting avoids this residual. AUTH-MAIL-002C2 — Handle verification links safely without exposing action codes #194 adds no storage write, explicitly tests deletion, and now removes the capability from both native history and React Router state.

    The accepted #194 boundary is: no new capability persistence; immediate native/router scrub; no action-code check/apply on scanner-style load; and a documented existing Pages residual pending a direct SPA rewrite.

  8. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    EXTRACTION FINISHED — 2026-07-14T00:11:56Z

    Branch codex/issue-194-verify-email-action is pushed at exact commit 1eae2d14917406cd9ee5e05224c07ac47d1558ae. Exact staged diff SHA-256: 8d82d831b734ba75823fa70cb447b6b07b668ed3436f34949412963567000180.

    Local evidence on Node 20 / Java 17:

    • focused Auth/action/App Check/Sentry/App route suites: 111/111
    • complete frontend: 11 suites, 198/198
    • SPA handoff: 11/11
    • Functions: lint plus 16 suites, 990 passed / 32 emulator-only skipped
    • Firestore Rules emulator: 317/317
    • commerce journal real emulator: 31/31
    • release/workflow/artifact guards: 42/42
    • exact lint baseline: 104 files, 125 existing errors, 7 existing warnings, zero new findings
    • strict changed-source lint, diagnostic production build, relative Markdown links, diff check, secret-shape scan: pass
    • independent security, test, and backup-officer reviews: APPROVED on the exact diff

    Deployment truth: source and tests only. No website, Firebase, provider, mailbox, template, DNS, member data, or production behavior changed or was tested.

  9. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    DELIVERY COMPLETE — 2026-07-14T00:17:06Z

    PR #195 merged as exact main e76b1ee01a0f0ce9951fdcc6e454b66288394b93. Exact-head PR CI run 29295281279 passed frontend, Functions, Firestore Rules, commerce emulator, and artifact jobs; the Netlify PR preview was ready. Post-merge CI run 29295389626 passed the same five jobs.

    Three independent exact-diff reviews approved security, test completeness, and backup-officer usability. The final issue acceptance is source/test/merge only, as designed. The claim and status:in-progress label are released.

    Deployment evidence: not published. The exact merge commit has only a CI run. The repository still has no protected production environment (only github-pages) and no repository release variables/secrets. runmprc.com still serves main.58ede287.js, not the locally proven #194 build main.fdd058b4.js. No Firebase deployment, action-handler/template/sender/DNS setting, mailbox test, or live action-code behavior occurred. The route must remain out of Firebase's global custom-handler configuration until #119 proves all enabled modes are safe.

  10. daliu commented on Jul 17, 2026

    @daliu
    ContributorAuthor

    #335 reused #194's dual native-history/React-Router cleanup pattern for the separate Strava callback and merged via PR #336 as exact main f9a3f3cac92ae1a38d4a02041cf7301355470f36. A review-driven distinction is explicit: unconfirmed Router cleanup waits with callback hooks unmounted; detected ineffective/throwing/path-divergent cleanup shows a fixed stop. #194 files and behavior were unchanged.

    Exact-main CI 29555712712 passed all five jobs; deployments are empty.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:authAuthentication and authorizationarea:firebaseFirebase services and dataarea:webWeb application and hostingbugSomething isn't workingpriority:P1High-priority follow-upsize:MMedium multi-file issuetype:reliabilityReliability and recoverytype:securitySecurity or privacy boundarytype:testingTest infrastructure and quality gates

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions