No description
  • Python 54.9%
  • PHP 44.1%
  • Shell 0.5%
  • Dockerfile 0.3%
  • CSS 0.2%
Find a file
2026-10-23 23:49:51 +09:00
dev [verified] preserve lazy AuthManager initialization with LoginNotify 2026-10-23 23:49:51 +09:00
docs [verified] harden signup verification and finalize release metadata 2026-10-23 21:20:22 +09:00
i18n [verified] require email proof before MediaWiki account creation 2026-10-08 20:37:19 +09:00
includes [verified] preserve lazy AuthManager initialization with LoginNotify 2026-10-23 23:49:51 +09:00
maintenance [verified] harden signup verification and finalize release metadata 2026-10-23 21:20:22 +09:00
resources [verified] harden signup verification and finalize release metadata 2026-10-23 21:20:22 +09:00
sql [verified] require email proof before MediaWiki account creation 2026-10-08 20:37:19 +09:00
tests/phpunit [verified] preserve lazy AuthManager initialization with LoginNotify 2026-10-23 23:49:51 +09:00
tools [verified] harden signup verification and finalize release metadata 2026-10-23 21:20:22 +09:00
.gitignore chore: ignore generated Python caches and local dependencies 2026-10-08 14:10:54 +09:00
extension.json [verified] harden signup verification and finalize release metadata 2026-10-23 21:20:22 +09:00
LICENSE [verified] harden signup verification and finalize release metadata 2026-10-23 21:20:22 +09:00
README.md [verified] preserve lazy AuthManager initialization with LoginNotify 2026-10-23 23:49:51 +09:00
ServiceWiring.php [verified] harden signup verification and finalize release metadata 2026-10-23 21:20:22 +09:00
SignupEmailVerification.alias.php [verified] harden signup verification and finalize release metadata 2026-10-23 21:20:22 +09:00

SignupEmailVerification

Latest UI correction: failure/reverification guidance remains on the signup and recovery forms but no longer appears after successful signup. Current-source validation passed 603 tests per SQLite/MariaDB backend and 20/20 native browser E2E checks across English desktop and Korean mobile, including real SMTP-code verification, signup, logout and password re-login. Independent correction SPEC, quality/security and saved-evidence reviews passed. See correction verification and limits.

The broader extended QA, initial browser E2E and 40-item edge audit are preserved historical source checkpoints. Their 72 native DB executions, 224 HTTP checks and 16 HTTPS/shared-session cases were not rerun for this display-only correction. Requirements are SQLite (core minimum), MariaDB 10.6+, or MySQL 8.0+; native validation used SQLite 3.46.1 and MariaDB 11.4.13. No actual-production validation or deployment is claimed.

Require proof of mailbox access before MediaWiki inserts a local user row. Intended for MediaWiki 1.45.4; the manifest bounds compatibility to >= 1.45.4 < 1.46.0. Only 1.45.4 has been exercised.

Flow

  1. Special:CreateAccount redirects an unverified session to Special:VerifySignupEmail (특수:가입이메일인증 in Korean).
  2. A POST with a core session CSRF token requests an email code. No username, password or user row is created.
  3. Enter the 16-character hexadecimal code in the same browser session. Successful verification grants a short-lived, single-use authorization and opens the normal core signup form.
  4. The signup form prefills the verified email address as a required, read-only field and omits the unsupported mail-a-password option (including for administrators). The preauthentication provider still checks the submitted email before primary account creation; the final AuthManagerVerifyAuthentication hook conditionally consumes proof before User::addToDatabase. UI changes do not authorize an API request or a forged email value. Core stores the email as authenticated. The no-email authentication-session flag suppresses core's duplicate confirmation email.

The Action API action=createaccount is subject to the same gate. First verify through the special-page forms with the same cookie jar, obtain a createaccount token, then call core's API with username, password, retype, email, realname (empty is allowed), createtoken and createreturnurl. This extension does not collect or persist passwords.

Installation

Place this directory at extensions/SignupEmailVerification. In LocalSettings.php:

wfLoadExtension( 'SignupEmailVerification' );
$wgEnableEmail = true;
// Configure your site's real mail transport and PasswordSender separately.

Run the core schema updater from the MediaWiki root before accepting signup traffic:

php maintenance/run.php update --quick

If $wgAuthManagerConfig is explicitly set, MediaWiki does not merge the auto-config registration. Include MediaWiki\Extension\SignupEmailVerification\PreAuthenticationProvider in its preauth specifications with sort -100. Do not remove, replace or filter this provider, and keep the extension's hooks enabled. The extension installs GuardedAuthManager, preserving core's lazy provider initialization. When providers are first initialized, it checks the actual preauthentication array after all AuthManagerFilterProviders handlers have run, before primary/secondary providers are constructed or account creation can proceed. A missing or different provider throws a configuration error, including maintenance/session autocreation and the real TempUserCreator path. Service retrieval itself does not initialize providers: dependencies such as LoginNotify.LoginNotify can safely retrieve the cached manager. The filter contract can only remove configured specifications, not inject a missing one; the extension therefore rejects an unsafe configuration rather than silently repairing it or trusting the final signup hook alone. This invalid configuration also makes existing authentication/password reset unavailable until corrected; correctly registered default and explicit configurations leave those flows unchanged. This registration check does not consume email proof.

GuardedAuthManager mirrors MediaWiki 1.45.4's constructor wiring and overrides the protected providerArrayFromSpecs() boundary. Do not replace the AuthManager service or bypass this subclass; coordinate any extension requiring its own replacement. Recheck this non-stable core boundary and constructor wiring before upgrading core. The manifest intentionally limits support to MediaWiki >= 1.45.4 and < 1.46.0.

Database requirements and validation limits

  • SQLite: use a version supported by MediaWiki 1.45.4 core; this extension does not impose a separate SQLite minimum or issue a SQLite server-version query.
  • MariaDB >= 10.6.0 or MySQL >= 8.0.0, using transactional InnoDB tables. Nontransactional tables and other database engines are unsupported.

These extension minimums are stricter than core's MariaDB 10.3.0 / MySQL 5.7.0 minimums. Cleanup uses SELECT ... FOR UPDATE SKIP LOCKED so that expired rows locked by concurrent issuance/verification are deferred instead of introducing an inverted lock wait. MariaDB introduced this syntax in 10.6; its SELECT reference limits it to InnoDB. The MySQL 8.0 locking-read reference documents the same row-skipping behavior. Locked eligible rows can remain after a sweep and must be reclaimed by a later run; this is not a guarantee of immediate deletion or freedom from every possible deadlock.

Before returning the SignupEmailVerification.Store service and before registering extension schema updates, a shared compatibility guard rejects unsupported engines, legacy MySQL/MariaDB versions, and unrecognized server-version metadata with a clear LogicException describing the requirements. It distinguishes MariaDB from MySQL, accepts the documented MariaDB 5.5.5- compatibility prefix, and compares the numeric version rather than casting arbitrary text. Existing installations on older servers must upgrade the database explicitly before using the extension; there is no automatic database upgrade, blocking cleanup fallback, or legacy compatibility mode. InnoDB remains an operator requirement, not a table-engine audit performed by this version guard.

Native validation remains limited to MediaWiki 1.45.4 with SQLite 3.46.1 and MariaDB 11.4.13 (InnoDB), including actual HTTP signup and native-database contention tests. Oracle MySQL itself and other database versions have not been exercised. Boundary/version-parser tests use synthetic server metadata against the actual core interfaces; they are not native tests on the older/minimum servers. Meeting a declared minimum is not a claim that every version, isolation mode, replication setup or extension stack has been validated. Stage and review on your actual deployment stack. See the final edge-case verification and the earlier compatibility checkpoint. The final verdict is limited to the documented scope and is not production-deployment approval. No production deployment is performed by the development harness.

Configuration

All names below have the $wgSignupEmailVerification prefix.

Suffix Default Meaning
CodeTTL 900 Code lifetime in seconds
GrantTTL 900 Proof lifetime after verification
Cooldown 60 Minimum interval between sends, per email and session
MaxCodeFailures 5 Maximum code submissions per challenge; a successful attempt also uses a slot
RateWindow 3600 Fixed-window duration in seconds
SendLimits [ 'Email' => 5, 'IP' => 20, 'Session' => 5 ] Send attempts per window
VerifyLimits [ 'Email' => 20, 'IP' => 100, 'Session' => 20 ] Verification attempts per window, shared across reissues
SecretKey null Optional dedicated key; otherwise use core $wgSecretKey

TTL, cooldown, failure and window values must be integers from 1 to 86400. Each limits array must contain exactly the three named scopes, with integer limits from 1 to 100000. The selected secret must contain at least 32 bytes; use a cryptographically generated secret, not a human password. Invalid settings throw a configuration error and fail closed. Disabled core email prevents issuing, verifying and consuming proof. Rotating the key invalidates existing session bindings/codes and resets HMAC rate namespaces.

Rate state is stored on the primary database, not a worker-local cache. Conditional increments reserve slots under an atomic section. Earlier scopes may remain charged when a later scope rejects, and cooldown-denied sends may consume budget: conservative throttling is intentional. Verification with no challenge still charges IP/session scopes. Fixed windows allow boundary bursts. Each resend invalidates previous pending/verified challenges for that session, including for a different mailbox. Transport errors and exceptions invalidate the new challenge and do not reveal delivery status.

Consume is one conditional verified -> consumed database update, checked for exactly one affected row. It participates in core's request transaction, not an independent permanent commit. If core rolls back user creation, the transaction may also roll back consumption. The extension never manually reactivates a proof. A failed signup may still consume the proof, so you may need to return to Special:VerifySignupEmail, request a fresh code and verify again in the same browser session before retrying signup. Both verification and signup pages show this guidance in English and Korean with a link that contains no email address or token. A committed successful signup cannot reuse it for a different username.

Security and supported paths

  • Local password signup only. An unknown configured primary provider causes creation to fail before its primary creation routine is invoked, not just after an external account may have been created. Allowed core primary class names may use one leading namespace separator and PHP's case-insensitive spelling; whitespace, repeated separators, unknown classes, subclasses and factory specifications remain unsupported. This is a class-name contract, not normalization of provider IDs: the final hook still requires the exact core local-password primary ID. Core temporary-password account creation/mail-a-password is not supported. Provider constructors/factories are configured code and may run during core initialization; the early check is not a sandbox for untrusted provider construction.
  • Administrator UI/API signup has no exemption. Administrators need same-session proof for the requested address too.
  • AuthManager autocreation (session, maintenance and temporary-user sources) is denied. This is intentionally incompatible with temporary-user and external/SSO auto-provisioning.
  • Existing login is unchanged. Direct calls to User::createNew/addToDatabase, raw database inserts, and third-party code bypassing AuthManager are outside the guarantee. Audit other installed extensions for these paths.
  • Email normalization trims whitespace and lowercases only the domain. It preserves local-part case and does not fold plus-addressing, aliases or provider-specific mailbox semantics. No lifetime accounts-per-mailbox cap is imposed.
  • Codes use 8 random bytes; challenge/session-binding secrets use 32 random bytes. Codes are stored as purpose-separated HMACs; raw codes are returned only to the mail caller, not written into session/database. Rate bucket values hide raw IP/email identifiers with HMACs. Challenge email addresses remain personal data in the database.
  • Use HTTPS and a secure, shared core session store. Configure trusted reverse proxies correctly: the IP limit uses core WebRequest::getIP(). Do not blindly trust forwarded IP headers. Restrict database and SMTP access; avoid logs of mail bodies/tokens.
  • This adds cost to mass registration, not a universal bot solution. Unlimited mailboxes, distributed IPs and new sessions can evade individual scopes. CAPTCHA, IP/site-wide limits, abuse monitoring and operational controls remain complementary. A shared IP limit can affect legitimate NAT users.

Cleanup

Run from the MediaWiki root, using the actual absolute extension path:

php maintenance/run.php /path/to/mediawiki/extensions/SignupEmailVerification/maintenance/cleanup.php --retention 86400 --limit 500

Each run deletes at most limit rows per table. Only challenges whose code and grant expiry are at/before now - retention, and rate buckets whose expiry is at/before that cutoff, can be removed. It never deletes a live grant/window. Repeat batches or schedule periodic runs. Retention is 0..31536000 seconds; limit is 1..10000. Cleanup reports counts only, not email addresses or secrets.

Local development and tests

tools/mw-dev uses pinned MediaWiki 1.45.4, PHP 8.3, SQLite and an isolated Mailpit SMTP capture. Published ports bind only to loopback; wiki/mail services have no outbound network. The repo is mounted read-only into the runtime; edit files on the host. Synthetic credentials and generated evidence are ignored under dev/.state/ or the disposable wiki-state volume; do not print credentials or attach debug logs publicly.

./tools/mw-dev up
# Set MW_LOAD_EXTENSION=1 in the ignored dev/.state/runtime.env, then recreate the wiki.
./tools/mw-dev exec php maintenance/run.php update --quick
./tools/mw-dev phpunit extensions/SignupEmailVerification/tests/phpunit
python3 dev/tests/signup_e2e.py
./tools/mw-dev smoke
./tools/mw-dev exec php vendor/bin/phpcs --standard=MediaWiki \
  extensions/SignupEmailVerification/includes extensions/SignupEmailVerification/maintenance \
  extensions/SignupEmailVerification/tests/phpunit extensions/SignupEmailVerification/ServiceWiring.php \
  extensions/SignupEmailVerification/SignupEmailVerification.alias.php

HTTP E2E uses real core HTML forms, SMTP messages and ActionAPI requests, and reads exact SQLite rows back. It checks CSRF/invalid email/bad code, no-proof denial, confirmed UI and API accounts, no duplicate confirmation mail, wrong email/session/reuse denial, and two parallel HTTP requests attempting different usernames with one proof. These are HTTP form tests, not browser-driven visual tests. The parallel test exercises the installed SQLite/session stack; it is not proof of MariaDB parity or of every isolation mode. Repeated E2E runs consume the actual IP budget and can throttle; reset the disposable runtime or wait for the next window rather than weakening extension limits.

Core PHPUnit normally replaces extension AuthManager providers with test defaults. Integration tests explicitly restore auto-config, use real core account creation, and suppress mail except the local SMTP smoke. The standalone local SMTP/E2E checks inspect fresh captured message IDs, not old mail. All Python loopback clients disable inherited proxies with ProxyHandler({}).