Historical 0.1.0 implementation audit. For current implementation status and real PHP reference results, read CURRENT.md and VERIFICATION.md. The source inventories below remain pinned to the same upstream commit.
Compatibility audit
Source pinned to 3516c776a1fcbd83d234efc007e3a49807d6ed91 (2FAuth upstream). All source paths below are relative to upstream/.
routes.json inventories 91 application method/path declarations, including expanded apiResource methods, inherited route middleware, constraints and source lines. GET also accepts HEAD. Kernel global/group/alias/priority declarations are included verbatim. This is static source evidence, not Laravel's effective route list: alias expansion, priority sorting, duplicate removal, package routes, CORS preflight and framework fallback behavior still require a booted reference. The API prefix is /api/v1; settingName is globally [a-zA-Z]+ (app/Providers/RouteServiceProvider.php). API unknown-route text is literally unknowkn endpoint.
database.json records all 42 ordered migration sources and hashes, including data backfills. It deliberately does not claim to be a materialized SQL schema. environment.json maps 170 literal configuration environment names to source locations without reading deployment values; dynamic preference names are listed separately. Regenerate from the repository root:
python rust-server/tools/inventory.py
Drop-in requirements and blockers
| Area | Required behavior and evidence | Unverified work before a drop-in claim |
|---|---|---|
| Existing databases | SQLite, MySQL, PostgreSQL and SQL Server configured in config/database.php. Users, options, accounts, groups, icons, sharing, per-user assignment/order, authentication/OTP logs, sessions/cache, OAuth and WebAuthn tables are migration-backed. |
Apply real migrations against fixture databases for each supported driver, compare types/defaults/nullability/indices/foreign keys and preserve migration history. Static declarations are not sufficient. |
| Schema evolution | 2026 group assignment/order migrations backfill legacy values then drop twofaccounts.group_id and order_column. Passport 13 migration changes clients to owner morphs/redirect_uris/grant_types and drops oauth_personal_access_clients. WebAuthn 2022 migration uses a helper defined after down(). |
Inspect full helper source and data transformations, exercise upgrades and use current table shapes. Do not recreate the original 2019 account schema as the current schema. |
| Encryption | config/app.php selects AES-256-CBC, APP_KEY and APP_PREVIOUS_KEYS. app/Models/Traits/CanEncryptField.php invokes Laravel Crypt encryptString/decryptString conditionally on useEncryption; account model applies it to fields and pads secrets as Base32. |
Implement and test Laravel ciphertext envelope/MAC, key parsing/rotation and actual encrypted fixtures. Returning stored ciphertext as an OTP secret is not compatible. Preserve key and option state. |
| Authentication | config/auth.php: web-guard session, api-guard Passport, reverse-proxy-guard custom remote-user provider. CustomCreateFreshApiToken mints the first-party laravel_token for authenticated responses. PreventRequestForgery enables XSRF cookie with no exclusions. |
Verify encrypted/signed cookies, session persistence, CSRF exchange (419), JWT/PAT validation/revocation/scopes, password hashing, reset and recovery brokers and remember-me flows. An arbitrary bearer-token lookup is not Passport compatibility. |
| Authorization and sharing | app/Policies/{TwoFAccount,Group,User}Policy.php, sharing middleware and model scoping enforce owner/shared recipient/admin rules. New shares/assignments/orders are per-user. |
Exercise cross-user negative tests, ownership transfer, all-user scope, admin-only paths, disabling sharing, SSO-only/demo/proxy modes and settings gates. |
| Middleware | Kernel.php, middleware sources and RouteServiceProvider implement trim/empty-to-null, forced JSON Accept, throttling, bindings, language, maintenance/CORS/proxy handling and priority. KickOutInactiveUser returns 418 and updates last_seen_at; bearer/proxy users bypass its inactivity tracking. |
Verify actual ordering/status/envelopes/cookies. Preserve rejectIfReverseProxy, rejectIfSsoOnlyAndNotForAdmin, forceLogout and admin policy behaviors. |
| Frontend | resources/views/landing.blade.php injects appSettings/appConfig/urls/defaultPreferences/lockedPreferences/appVersion/isDemoApp/isTestingApp/appLocales, CSRF meta and optional CSP nonce. SinglePageController computes visitor-safe settings and subdirectory paths. Existing public/build/manifest.json and assets are present. |
Reuse existing build plus public assets and reproduce bootstrap shape/CSP/base URLs; an unrelated minimal page is not the existing UI. Source rebuild needs sibling ../2FAuth-Components/packages/{formcontrols,stores,styles,ui} referenced by package.json. |
| Frontend HTTP contract | resources/js/services/httpClientFactory.js uses credentials, JSON, X-Requested-With, web root vs /api/v1, retries 419 via refresh-csrf, handles 418/407/401/422/404 distinctly. |
Browser login, refresh, page reload, account/group/share/icon workflows and translated validation payloads must be tested. |
| OTP and imports | app/Models/TwoFAccount.php supports HOTP/TOTP/Steam, next-password payloads, URI parsing/exports; API controllers/resources/requests and service classes define wire format and validation. |
Verify SHA algorithms/digits/period, HOTP increment atomicity, Steam, QR image decode/encode, encrypted import/export variants and field/error envelopes against real source fixtures. |
| Integrations/operations | WebAuthn, Socialite OpenID/GitHub, proxy auth, mail notifications/reset, logo fetch, content policy, scheduled log cleanup, cache/optimization and CLI installation are upstream functionality. | A route that returns an explicit unsupported error is honest partial coverage, not parity. All enabled deployment features need implementation and integration tests. |
Differential HTTP harness
The stdlib-only tools/differential_http.py compares two separate disposable instances with equivalent fixture data, reporting only case numbers, status codes, comparison booleans or sanitized exception class names. It never prints response bodies, header values, environment values or URLs. Redirect following and retries are disabled. Body limit is 4 MiB. TLS verification remains enabled. Credentials are supplied by environment variable names referenced by a case file, separately for each instance. Cookies are retained only with the explicit --sessions --allow-side-effects flags, in separate in-memory jars per instance; they are never written to disk.
python rust-server/tools/differential_http.py \
--reference http://127.0.0.1:8000 \
--candidate http://127.0.0.1:8080
The four original default probes are GET /up, /manifest.json, /api/v1/user/name, and an unknown API path. Three additional defaults check CORS preflight, a cross-origin unauthenticated GET and the same GET without an Origin header. They can create framework sessions/rate-limit counters; no live production instance should be used. /up may differ in the framework-rendered HTML even if status is equivalent; exact body comparison intentionally exposes that difference.
All nondefault paths, authenticated requests and mutations outside the built-in safe probes are skipped unless --allow-side-effects is supplied. GET alone does not establish safety: upstream logout, optimize, clear-cache, HOTP and activity tracking can mutate state. The illustrative cases.example.json includes a public synthetic OTP secret and a real account-creation mutation. Use it only with disposable fixtures and explicit opt-in:
python rust-server/tools/differential_http.py \
--reference http://127.0.0.1:8000 \
--candidate http://127.0.0.1:8080 \
--cases rust-server/compatibility/cases.example.json --allow-side-effects
Cases support json, headers_env.reference/headers_env.candidate, ignore_json_pointers and compare_headers. Stateful sequences additionally support capture_json (capture name to JSON pointer), {{capture_name}} references in later paths/JSON, and headers_from_cookies (header name to cookie name, URL-decoded). Captures and cookie values remain separate per instance and are never printed. Missing captures/cookies fail the case. expected_status makes matching error responses fail when the scenario expected success. A whole JSON placeholder preserves its type; an embedded placeholder becomes text. Bodies are compared exactly after JSON parsing and replacing each configured base URL with <BASE>; explicit JSON pointers can exclude nondeterministic fields. Raw OTPs, timestamps, IDs, localized messages and random HTML CSP/CSRF values can differ. Do not indiscriminately mask these differences. Establish a known clock and matched database IDs/fixtures. Selected headers compare exactly in memory; Set-Cookie parity and browser token exchanges need separate stateful tests.
Exit code is 1 for any executed difference or transport failure, 0 when all executed comparisons match. Skips are reported and are never counted as evidence of coverage. A successful default run is seven transport/probe comparisons, not 91 route coverage and not a drop-in certification.
Verification performed
The inventory generator and harness syntax/help were checked. The harness was exercised against local deterministic stub servers for matching responses, divergent statuses, redirects, skipped mutation cases and sanitized error reporting. A later isolated Laravel 13.29.0 / PHP 8.4.27 reference boot applied all 42 SQLite migrations and ran four default, twelve basic stateful and 23 extended comparisons against the preexisting release binary. See reference-results.json for exact sanitized comparisons, binary hash and limitations. This does not certify the current edited Rust source or production compatibility.
Pinned frontend reuse implemented
src/frontend.rs now serves the unmodified upstream Vue build plus allowlisted public image/CSS/robots assets. public/UPSTREAM-LICENSE and UPSTREAM-NOTICE.txt retain the upstream provenance/license. frontend-assets.json records SHA-256 values for every copied public file. PHP is not copied or served. Three targeted frontend tests passed: malicious inline data escaping, visitor redaction, and actual session/cookie/CSP/static-file bootstrap. The module exposes routes() -> Router<AppState> and includes the explicit SPA paths from the upstream Vue router, the root alias, static files and manifest. Unknown API paths do not become HTML.
The HTML bootstrap uses the upstream global names and Vite manifest, default/locked user preferences, locales and visitor-safe settings (all nonvisitor option values become null). Inline JSON escapes HTML delimiters and Unicode line separators; title/theme/attribute values are separately HTML-escaped. A generated nonce protects both scripts under CSP. The HTML handler calls the actual native-session auth::refresh_csrf, includes its CSRF token in the meta tag and preserves its session/XSRF cookies. It does not invent a separate frontend authentication token. The copied Axios client reads XSRF-TOKEN and submits its same-origin X-XSRF-TOKEN header; login returns the expected user payload and rotates cookies. The upstream authGuard restores current-user state from GET /api/v1/user and then fetches accounts/groups.
RUST_PUBLIC_DIR can relocate the public asset directory; it defaults to the crate's public directory. APP_NAME, default/locked preference variables and CONTENT_SECURITY_POLICY affect the bootstrap. Native sessions default to Secure cookies; HTTP localhost development requires the explicit RUST_COOKIE_SECURE=false setting from the auth module. Nonempty APP_SUBDIRECTORY fails with 503 because mounting only the HTML subpath would leave auth/API/assets inconsistent. Proxy and SSO provider capabilities are not advertised by the experimental bootstrap because those authentication methods are unimplemented. Interface links can still reach unimplemented backend features; displaying the original UI does not establish drop-in backend compatibility.
Stateful comparison sequence
cases.stateful.json exercises CSRF cookie acquisition, password login, current user, HOTP creation, two OTP generations, counter readback, deletion/readback, logout and rejection after logout. Provision compat@example.invalid with the public synthetic password in that file on both disposable instances, with equivalent nonadministrator fixture users and matching options. Run with:
python rust-server/tools/differential_http.py \
--reference http://127.0.0.1:8000 --candidate http://127.0.0.1:8080 \
--cases rust-server/compatibility/cases.stateful.json \
--sessions --allow-side-effects
The sequence is a starting fixture, not recorded evidence of parity. Random CSRF response bodies and protocol differences intentionally remain visible; only explicitly listed account IDs are ignored in JSON comparisons while per-side IDs drive subsequent requests. It compares state effects through later API reads, not direct database snapshots. It does not establish database schema, cookie cryptography, token persistence, concurrency or browser parity. Timestamps remain unmasked; a clock control is still needed for time-sensitive responses.
python rust-server/tests/differential_harness.py verifies typed/escaped JSON captures, separate cookies/IDs, URL-decoded CSRF header replay, mutation readback and expected-status failure against local deterministic stub servers. These are harness regression tests, not PHP-versus-Rust evidence.
The recorded reference run matched 8/12 complete stateful responses. Both HOTP values and counter readback matched; CSRF/login/error bodies still differ. All 12 expected statuses matched. Default probes matched 0/4 complete responses, including PHP 200 versus Rust 404 on the deprecated-name route. These are limited case results, not route coverage percentages.
cases.extended.json adds group create/read/delete, administrator denial, disabled-sharing denial, QR encode and invalid decode, import/invalid-account validation and feature flags. The recorded baseline matched 11/23 complete responses; unimplemented routes and validation differences remain visible.
For a reproducible local run, install the pinned upstream/composer.lock dependencies with PHP 8.4+ and all extensions in composer.json, then run:
python tools/run_reference.py --php /path/to/php \
--source ../upstream --candidate-binary target/release/twofauth-rs \
--output compatibility/reference-results.local.json
The runner creates a new temporary reference application and two fresh SQLite databases, executes all upstream migrations, creates Passport fixture keys and public synthetic regular/admin/recipient users, then launches both servers and runs 99 core comparisons across the four suites plus three CORS transport probes (102 total). It destroys the fixture directory afterward. Source vendor must already contain Composer dependencies; it never installs software or changes the source database/configuration. Generated source config/route caches, environment files and existing session/cache state are excluded. Only PHP runtime environment variables are inherited, so deployment configuration/secrets are not reused. Export PHPRC if your PHP runtime needs a custom extension configuration, including for child PHP processes. The result includes the candidate binary SHA-256. Exit 1 means actual compatibility differences; exit 2 means setup failed. No production service is involved.
Use --candidate-db laravel-copy to test against a SQLite backup of the real migrated PHP fixture schema. In that mode the runner preserves the PHP user/password record and invokes prepare-sqlite to add the Rust authentication sidecars. Use --candidate-db native (the default) for the independent development schema. Both modes use new disposable databases; neither accepts a deployment database path.
sqlite-reference-schema.sql records the actual SQLite DDL produced by all 42 migrations (22 application tables, 34 explicit indexes), without fixture rows. The DDL was replayed successfully into an empty SQLite database. It documents this driver only; MySQL, PostgreSQL and SQL Server still require real materialization and comparison.
cases.admin-sharing.json adds administrator settings/user mutations and multi-user sharing checks. The runner creates a regular user, an administrator and a recipient on both servers, preserving the original regular-user scenarios. It exercises custom setting CRUD, actual encryption on/off with account readback, last-administrator protections, user creation/promotion/demotion, empty PAT revocation, explicit/all-user sharing, secret suppression, export denial, deterministic HOTP, visible counts, independent per-user groups, revocation and self-transfer validation. Role switches explicitly refresh CSRF cookies before login. JSON assertions independently check missing secrets, RFC HOTP, counts and owner group independence. The fixture sets THROTTLE_API=1000 to accommodate the bulk sequence; default rate-limit equivalence is a separate test requirement.
Add --semantic-checks to preserve every exact comparison and separately check three nondeterministic representations: health HTML equality after changing only the numeric render duration, textual CSRF token shape plus successful subsequent login, and both rendered QR payloads. QR checks require Node sharp and use the reference PHP Zxing\QrReader to decode each SVG after rasterization. Both decoded provisioning URIs must match each other and the known synthetic fixture. Render/decode failures are reported explicitly. Raw QR bytes, random CSRF strings and health render times remain visible as exact differences; semantic results never replace them.
Static headers are supported for synthetic protocol probes; header values are never printed. expected_headers, expected_json, absent_json_pointers and expect_text_token add assertions independently of response equality. The three CORS probes compare status and allow-origin/method/header behavior, assert wildcard origins without credentials and assert the source wildcard header is retained even when Origin is absent. The original 99 core case count is retained separately from these three added transport checks.
Historical 0.2.0 reference evidence
The 0.2.0 run, retained in VERIFICATION-0.2.0.md, used independent native and copied Laravel SQLite fixture schemas. It executed 102 comparisons per schema (99 core cases plus three CORS probes), with 91 exact responses each. The filenames reference-results.native.json and reference-results.laravel.json now hold the latest release results; see VERIFICATION.md for their current counts and hash. All statuses, content types, selected/expected headers and independent scenario assertions matched. CORS preflight, actual cross-origin requests and requests without Origin matched exactly.
The eleven remaining exact body differences per schema are nine independently generated CSRF strings, one health render duration and one QR image encoding. Separate checks passed for health equality after changing only the render-duration number, valid textual CSRF tokens followed by successful login, and both rendered QR images decoding to the same expected synthetic provisioning URI. Raw differences remain recorded. No cases were skipped and no transport/capture errors occurred. These finite fixtures do not certify every route, integration, deployment configuration or supported database driver.
reference-results.json preserves the earlier baseline and upstream-suite/DNS limitations; it is not the final release report. Both final reports carry the candidate SHA-256, harness/case hashes and explicit fixture configuration.