=== SPIDY 2FA LOGIN ===
Contributors: carlocozzetto
Tags: two factor authentication, 2fa, otp, sms authentication, login security
Requires at least: 6.0
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 1.6.12
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Two-factor authentication (2FA) via email, SMS or biometric login (WebAuthn/FIDO2) for WordPress. Simple, free, no third-party app required.

== Description ==

**SPIDY 2FA LOGIN** adds a second layer of security to the WordPress login: after entering the correct password, the user is asked for a 6-digit code (email or SMS) or a biometric confirmation (fingerprint / Face ID via WebAuthn), depending on the method chosen for that account.

= Why it's different =

Most two-factor plugins fall into one of two categories: dedicated 2FA plugins that rely on a separate authenticator app (Google Authenticator, Authy, and similar TOTP apps the user has to install and set up before they can even log in), or full security suites where 2FA is one small feature buried inside a much heavier toolkit of malware scanning, firewalls, and file monitoring — useful if you need all of that, overkill if you just want a second login step.

SPIDY 2FA LOGIN doesn't ask the user to install anything. Email and SMS just work with a code. Biometric login uses the device's own fingerprint sensor, Face ID, or a passkey already saved in the browser — the same technology behind passwordless sign-in on major websites — with automatic fallback to email if it's ever unavailable, so nobody gets permanently locked out. Three methods, one plugin, nothing extra to install on either side.

= What's included =

* Two-factor authentication via email code
* Optional SMS delivery via your own Twilio or Clickatell account (pay-as-you-go pricing set by the provider you choose)
* Biometric login (fingerprint / Face ID / passkey) via the WebAuthn/FIDO2 standard — requires PHP 8.2+ and an HTTPS site; falls back to email automatically if unavailable or no device is registered
* Simple, immediate user management panel with per-user method selection
* No user cap
* Translation-ready (all strings use the `spidy-2fa-login` text domain) — ready to receive community translations via translate.wordpress.org once listed

= Testing environment =

This plugin has always been developed and tested exclusively on live, publicly reachable WordPress and WooCommerce installations — never on a local or offline environment. Testing specifically took place on a subdomain (not the root/apex domain), which is relevant for the Biometric method: WebAuthn/FIDO2 scopes credentials to the Relying Party ID actually serving the page, so behavior on a subdomain reflects real-world multisite/staging-style setups rather than only the simplest single-root-domain case. This is stated here for accuracy, since local setups can hide issues (HTTPS, real SMTP delivery, actual mobile authenticators, real domain scoping) that only show up under real-world conditions.

= External services =

This plugin sends emails using WordPress's native `wp_mail()` function: no data is transmitted to third parties for this feature.

The Biometric login method (WebAuthn/FIDO2) never sends any data to third parties: the cryptographic verification happens entirely on your own server and the user's own device.

If you configure SMS delivery, the 6-digit login code and your site name are sent to **your own Twilio or Clickatell account** (whichever you choose) each time a user with SMS as their method and a saved phone number logs in. Message delivery, retention, and pricing are governed by the provider you choose — see [Twilio's privacy policy](https://www.twilio.com/en-us/legal/privacy) or [Clickatell's privacy policy](https://www.clickatell.com/help-center/policy-terms-and-conditions/what-is-your-policy-on-user-information-and-privacy/).

== Screenshots ==

1. Control panel — general settings
2. Control panel — email and SMS configuration
3. Control panel — biometric login (WebAuthn) setup
4. Control panel — security and emergency access options

== Changelog ==

= 1.6.12 =
* SECURITY FIX: the login interception hooked into `wp_authenticate_user`, a WordPress core filter that fires *before* the password itself is checked (`wp_check_password()` runs afterwards, inside `wp_authenticate_username_password()`). Combined with the redirect-and-`exit()` used to send the user to the 2FA verification page, this meant the second factor — and the OTP email/SMS send that goes with it — could be triggered by a valid username alone, with any password, since the password was never actually checked in that request. Not a full authentication bypass (the real OTP code, or a registered biometric device, is still required to finish signing in), but it allowed username enumeration and could be abused to trigger unwanted OTP emails/SMS at will (a direct cost where a paid SMS gateway is configured). Fixed by moving to the `authenticate` filter at priority 30 (after WordPress's own core password checks, which run at priority 20), so the code guarantees `$user` is only ever a real `WP_User` when both username and password have already been verified successfully — matching the fix already applied on a different branch of this plugin, ported back here.
* Fixed: leftover `Domain Path: /languages` header pointing to a folder that isn't bundled with the plugin (already listed as fixed in the 1.6.1 changelog entry below, but still present in this build).
* Verified compatibility with WordPress 7.0.4 (security-only release for Imagick, unrelated to this plugin's code) — full login flow (email, SMS, biometric) re-tested after updating.

= 1.6.11 =
* Fixed: the plugin header still listed "Web, Data & Service" as Author, inconsistent with the "carlocozzetto" account actually used for the WordPress.org repository (readme.txt Contributors was already correct). Also removed the same outdated reference from the visible text on the plugin's admin page

= 1.6.10 =
* Fixed: the "select/deselect all" toggle and its label in the 2FA column header made the table look cramped and unbalanced, since the row spacing no longer had a per-row Save button to fill it out. Shortened to "Attiva 2FA", set fixed column widths, and centered the 2FA checkbox for a cleaner layout

= 1.6.9 =
* Changed: the user management table now saves all users in a single action ("Salva tutti") instead of one save button per row — edit as many users as you need, then save once
* Added: a "select/deselect all" toggle on the 2FA column header, to enable or disable two-factor authentication for every user at once before saving

= 1.6.8 =
* Improved: on smartphones with a built-in biometric sensor, the "Scan this QR code with your phone" section is no longer shown during Biometric login — it made no sense to ask users to photograph the very screen they were reading. The QR section still appears normally on desktop/PC, where it's meant to be used. Detection is intentionally conservative (requires both a working platform authenticator and a mobile-device signal), so laptops with Windows Hello or similar are unaffected.
* Changed: the "Verifica su questo dispositivo" button label was shortened to "Verifica ora" for consistency with the QR-page button, which already used the shorter wording.
* Fixed: the internal `SPIDY_WP_VERSION` constant (used for cache-busting of CSS/JS assets) was still hardcoded to 1.6.0 while the plugin header had advanced through several releases; now kept in sync with the plugin version.

= 1.6.7 =
* Removed the bundled `languages/` folder (translation files are managed by translate.wordpress.org, not shipped in the plugin package) — per WordPress.org Plugins Team review feedback.
* Removed unneeded development-only folders bundled inside `vendor/` (PHPUnit test helpers, Symfony contract test base classes) that don't belong in a production release.
* Fixed a dead link to Clickatell's privacy policy in the "External services" section.
* Re-verified nonce and capability checks across every AJAX endpoint (already in place; no code change needed here).
* Updated the bundled web-auth/webauthn-framework library from 4.9.3 to 5.3.5 (flagged as out of date in review) — no changes needed in our own code, which already used the modern serializer-based API that carries over unchanged in 5.x.

= 1.6.6 =
* Fixed: the plugin declared "Requires PHP: 8.2" as a hard activation requirement, even though only the Biometric method actually needs it — WordPress would refuse to activate the plugin at all on lower PHP versions, making the built-in graceful-degradation logic (Email/SMS still work below 8.2) unreachable. Corrected to "Requires PHP: 7.4"; the runtime check that hides Biometric on PHP < 8.2 is unchanged.
* Clarified in the readme that testing took place on a subdomain rather than a root domain, which is relevant to the Biometric method's WebAuthn Relying Party ID scoping.

= 1.6.5 =
* Fixed: device registration forced `authenticatorAttachment: 'platform'`, requiring the browser to use ONLY the device's built-in biometric sensor (Windows Hello/Touch ID/Face ID) — on a computer where it isn't set up, this surfaced as an OS-level "Turn on Windows Hello" prompt with no way to proceed otherwise. Removed the restriction: the browser now offers every available option (built-in sensor, USB security key, browser-synced passkey, phone via QR/Bluetooth), and the user picks whichever they actually have.

= 1.6.4 =
* Fixed: biometric device registration reported "Operazione annullata" even after a successful Windows Hello/Face ID ceremony. Cause: `window.prompt()` asking for a device name was called right after the async WebAuthn ceremony, and modern browsers (Chrome/Edge) block or auto-dismiss native dialogs in that situation, since the "user gesture" is considered already consumed. Replaced with an inline on-page input, which isn't subject to this restriction.

= 1.6.3 =
* Fixed: "Tested up to" was rejected for including a patch number (7.0.2); WordPress.org only accepts major.minor here, corrected to 7.0.
* Fixed: last remaining NonceVerification warning, a read-only check used only to add a CSS body class.

= 1.6.2 =
* Added: `languages/` folder with a complete English translation (134 strings) and the Italian source identity file, matching the current v1.6.2 codebase — the source strings inside the plugin remain Italian, but English is now the fallback for anyone whose site language isn't Italian, as required for WordPress.org submission.
* Fixed: "Tested up to" corrected to 7.0.2, the current WordPress patch release.

= 1.6.1 =
* Fixed: "Tested up to" header referenced a non-existent WordPress version; corrected to the current stable release.
* Fixed: removed the "Domain Path" header, which pointed to a non-existent `/languages` folder (no `.mo`/`.po` files were bundled). Corrected the description accordingly — the plugin is translation-ready, not yet pre-translated.
* Fixed: unsanitized `$_POST['transports']` value during WebAuthn credential registration; now sanitized before decoding and filtered against the list of valid WebAuthn transport values.
* Fixed: `wp_login` core hook manually fired after 2FA verification was flagged as an unprefixed custom hook; documented with an inline justification (it is WordPress's own hook, not a custom one).
* Fixed: local template variables not prefixed with the plugin's namespace.

= 1.6.0 =
* Complete rebuild starting directly from the last known-working biometric build, instead of layering changes on top of a separate codebase. Same three methods (Email / SMS / Biometric), SMS now via Twilio/Clickatell instead of SMSGate.
* Fixed: assertion verification during login always passed a null user handle to the WebAuthn library instead of the expected fallback, causing verification to fail for authenticators that don't send their own userHandle (e.g. Samsung Pass / Chrome on Android).
* Fixed: several time-limited values (pending login token, OTP code, attempt counter, WebAuthn challenges) were stored using either native WordPress transients (unreliable on this hosting) or option keys that could exceed the historical 64-character limit of the wp_options.option_name column. All of these now use the same short, hashed, direct-write storage.
* Fixed: HTTPS requirement no longer deactivates the entire plugin — only the Biometric method is affected; Email and SMS keep working over plain HTTP.
* Added PHP-version guard: on PHP below 8.2, the WebAuthn library is not loaded at all (would fatal-error), and only the Biometric method is hidden — Email and SMS are unaffected.
* Added an explicit "delete all data on uninstall" preference, honoured only when the plugin is deactivated and then deleted from the Plugins screen (never on a simple update).