Changeset 3752373
- Timestamp:
- 10/17/2026 09:24:28 PM (81 minutes ago)
- Location:
- sigil-2fa
- Files:
-
- 6 added
- 10 edited
- 1 copied
-
tags/0.4.0 (copied) (copied from sigil-2fa/trunk )
-
tags/0.4.0/includes/class-app-passwords.php (modified) (2 diffs)
-
tags/0.4.0/includes/class-cli.php (modified) (1 diff)
-
tags/0.4.0/includes/class-plugin.php (modified) (1 diff)
-
tags/0.4.0/includes/class-two-factor-import.php (added)
-
tags/0.4.0/includes/class-two-factor-migration.php (added)
-
tags/0.4.0/readme.txt (modified) (4 diffs)
-
tags/0.4.0/sigil-2fa.php (modified) (2 diffs)
-
tags/0.4.0/templates/import-two-factor.php (added)
-
trunk/includes/class-app-passwords.php (modified) (2 diffs)
-
trunk/includes/class-cli.php (modified) (1 diff)
-
trunk/includes/class-plugin.php (modified) (1 diff)
-
trunk/includes/class-two-factor-import.php (added)
-
trunk/includes/class-two-factor-migration.php (added)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/sigil-2fa.php (modified) (2 diffs)
-
trunk/templates/import-two-factor.php (added)
Legend:
- Unmodified
- Added
- Removed
-
sigil-2fa/tags/0.4.0/includes/class-app-passwords.php
r3631768 r3752373 11 11 private static ?App_Passwords $instance = null; 12 12 13 /** @var array<int, true> */ 14 private array $app_password_users = array(); 15 13 16 public static function instance(): App_Passwords { 14 17 if ( null === self::$instance ) { … … 20 23 private function __construct() { 21 24 add_filter( 'wp_is_application_passwords_available_for_user', array( $this, 'filter_available' ), 10, 2 ); 25 add_action( 'application_password_did_authenticate', array( $this, 'note_application_password' ) ); 26 add_filter( 'authenticate', array( $this, 'refuse_api_password' ), 40 ); 27 } 28 29 /** 30 * @param mixed $user 31 */ 32 public function note_application_password( $user ): void { 33 if ( $user instanceof \WP_User ) { 34 $this->app_password_users[ (int) $user->ID ] = true; 35 } 36 } 37 38 /** 39 * Refuse the account password on XML-RPC and REST for anyone with a second 40 * factor. 41 * 42 * Those requests never reach wp_login, where the challenge runs, so a 43 * password accepted there is a sign-in with one factor. Application 44 * passwords are the way in for API clients and are left alone. 45 * 46 * @param mixed $user 47 * @return mixed 48 */ 49 public function refuse_api_password( $user ) { 50 if ( ! $user instanceof \WP_User || ! self::is_api_request() ) { 51 return $user; 52 } 53 54 if ( isset( $this->app_password_users[ (int) $user->ID ] ) ) { 55 return $user; 56 } 57 58 if ( ! Providers::instance()->has_usable( (int) $user->ID ) ) { 59 return $user; 60 } 61 62 return new \WP_Error( 63 'sigil_api_password_refused', 64 __( 'This account uses two-factor authentication. Use an application password for API access.', 'sigil-2fa' ) 65 ); 66 } 67 68 public static function is_api_request(): bool { 69 return ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ); 22 70 } 23 71 -
sigil-2fa/tags/0.4.0/includes/class-cli.php
r3634087 r3752373 146 146 147 147 /** 148 * Import users' two-factor methods from another plugin. 149 * 150 * Reads the source plugin's data and never changes or deletes it. Anything 151 * already set up in Sigil is left alone, so it is safe to run again. 152 * 153 * ## OPTIONS 154 * 155 * <source> 156 * : Plugin to import from. 157 * --- 158 * options: 159 * - two-factor 160 * --- 161 * 162 * [--dry-run] 163 * : Report what would be imported without writing anything. 164 * 165 * [--user=<user>] 166 * : Only this user (ID, login, or email). 167 * 168 * ## EXAMPLES 169 * 170 * wp sigil import two-factor --dry-run 171 * wp sigil import two-factor 172 * wp sigil import two-factor --user=12 173 * 174 * @param list<string> $args 175 * @param array<string, string|bool> $assoc_args 176 */ 177 public function import( array $args, array $assoc_args = [] ): void { 178 if ( 'two-factor' !== ( $args[0] ?? '' ) ) { 179 \WP_CLI::error( 'Unknown source. Supported: two-factor' ); 180 } 181 182 $dry_run = ! empty( $assoc_args['dry-run'] ); 183 $ids = null; 184 185 if ( isset( $assoc_args['user'] ) ) { 186 $user = $this->resolve_user( (string) $assoc_args['user'] ); 187 if ( null === $user ) { 188 \WP_CLI::error( 'User not found.' ); 189 } 190 $ids = [ (int) $user->ID ]; 191 } 192 193 $report = Two_Factor_Import::run( $ids, $dry_run ); 194 195 $items = []; 196 foreach ( $report['users'] as $row ) { 197 $items[] = [ 198 'ID' => $row['user_id'], 199 'login' => $row['login'], 200 'status' => $row['status'], 201 'imported' => implode( ',', $row['imported'] ), 202 'already' => implode( ',', $row['already'] ), 203 'preferred' => $row['preferred'], 204 'flags' => implode( ',', $row['flags'] ), 205 ]; 206 } 207 208 if ( [] !== $items ) { 209 \WP_CLI\Utils\format_items( 'table', $items, [ 'ID', 'login', 'status', 'imported', 'already', 'preferred', 'flags' ] ); 210 } 211 212 $c = $report['counts']; 213 \WP_CLI::log( 214 sprintf( 215 '%sUsers: %d, not using Two Factor: %d, email: %d, authenticator: %d, backup codes: %d, already in Sigil: %d, flagged: %d, needs attention: %d', 216 $dry_run ? '[dry run] ' : '', 217 $c['users'], 218 $c['not_using'], 219 $c['email'], 220 $c['totp'], 221 $c['backup'], 222 $c['already'], 223 $c['flagged'], 224 $c['attention'] 225 ) 226 ); 227 228 if ( $c['attention'] > 0 ) { 229 \WP_CLI::warning( 'Some users had a method Sigil could not carry over. Two Factor keeps protecting them while it is active, so leave it active until they have set up a method in Sigil.' ); 230 return; 231 } 232 233 \WP_CLI::success( $dry_run ? 'Dry run complete. Nothing was written.' : 'Import complete. Two Factor data was left in place; deactivate Two Factor once you have checked the result.' ); 234 } 235 236 /** 148 237 * @return list<string> 149 238 */ -
sigil-2fa/tags/0.4.0/includes/class-plugin.php
r3635650 r3752373 67 67 Frontend::instance(); 68 68 69 require_once SIGIL_DIR . 'includes/class-two-factor-import.php'; 70 require_once SIGIL_DIR . 'includes/class-two-factor-migration.php'; 71 Two_Factor_Migration::instance(); 72 69 73 // Pro add-on, when present. The free build strips pro/ entirely, so this 70 74 // include only fires in the Pro distribution. -
sigil-2fa/tags/0.4.0/readme.txt
r3635650 r3752373 3 3 Tags: two-factor, 2fa, passkeys, authentication, security 4 4 Requires at least: 6.9 5 Tested up to: 7. 05 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0. 3.37 Stable tag: 0.4.0 8 8 License: GPL-3.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 83 83 = Does this work with application passwords, the REST API, and XML-RPC? = 84 84 85 Application passwords bypass 2FA by design, as that is how WordPress authenticates automated requests. The settings screen documents this, and application passwords can be disabled per role to close that path. 85 Application passwords bypass 2FA by design, as that is how WordPress authenticates automated requests. The settings screen documents this, and application passwords can be disabled per role to close that path. The account password itself is refused on XML-RPC and REST for anyone with a second factor, because those requests never reach the challenge. 86 86 87 87 = Which PHP version do I need for passkeys? = … … 97 97 Yes. Put `[sigil_2fa]` on a page. Users manage their methods from there, and anyone required to enrol is sent to that page rather than to the dashboard. 98 98 99 = Can I switch from the Two Factor plugin? = 100 101 Yes. With both plugins active, open Settings → Sigil and use Import from Two Factor, or run `wp sigil import two-factor` (add `--dry-run` to preview). Email codes, authenticator apps and backup codes come across, so people keep signing in with the same app. Anyone with a method that cannot be carried over is listed in the report, and Two Factor keeps protecting them while it stays active. Two Factor's own data is not changed. Once everyone on that list has set up a method in Sigil, deactivate Two Factor. 102 99 103 = Can I enforce 2FA only for administrators? = 100 104 … … 102 106 103 107 == Changelog == 108 109 = 0.4.0 = 110 * Moving from the Two Factor plugin no longer means everyone enrols again. Settings → Sigil and `wp sigil import two-factor` carry each user's email codes, authenticator app and backup codes across, so the same authenticator keeps working. Anyone with a method that cannot be carried over is named in the report and stays with Two Factor until they set one up in Sigil. Nobody is moved to email codes they did not already use, and Two Factor's own data is left as it was. 111 * While both plugins are active, Two Factor stands down for anyone Sigil already covers, so nobody is asked for a code twice. A notice offers the import until Two Factor is deactivated. 112 * Security: XML-RPC and REST no longer accept the account password from someone with a second factor. Those requests never reach the two-factor challenge, so a password alone was enough there. Application passwords still work. 104 113 105 114 = 0.3.3 = -
sigil-2fa/tags/0.4.0/sigil-2fa.php
r3635650 r3752373 3 3 * Plugin Name: Sigil – Passkeys and Two-Factor Authentication 4 4 * Description: Two-factor authentication for WordPress logins: passkeys, authenticator apps, backup codes and email codes, with per-role enforcement, multisite support and account recovery. 5 * Version: 0. 3.35 * Version: 0.4.0 6 6 * Requires at least: 6.9 7 7 * Requires PHP: 7.4 … … 18 18 defined( 'ABSPATH' ) || exit; 19 19 20 define( 'SIGIL_VERSION', '0. 3.3' );20 define( 'SIGIL_VERSION', '0.4.0' ); 21 21 define( 'SIGIL_FILE', __FILE__ ); 22 22 define( 'SIGIL_DIR', plugin_dir_path( __FILE__ ) ); -
sigil-2fa/trunk/includes/class-app-passwords.php
r3631768 r3752373 11 11 private static ?App_Passwords $instance = null; 12 12 13 /** @var array<int, true> */ 14 private array $app_password_users = array(); 15 13 16 public static function instance(): App_Passwords { 14 17 if ( null === self::$instance ) { … … 20 23 private function __construct() { 21 24 add_filter( 'wp_is_application_passwords_available_for_user', array( $this, 'filter_available' ), 10, 2 ); 25 add_action( 'application_password_did_authenticate', array( $this, 'note_application_password' ) ); 26 add_filter( 'authenticate', array( $this, 'refuse_api_password' ), 40 ); 27 } 28 29 /** 30 * @param mixed $user 31 */ 32 public function note_application_password( $user ): void { 33 if ( $user instanceof \WP_User ) { 34 $this->app_password_users[ (int) $user->ID ] = true; 35 } 36 } 37 38 /** 39 * Refuse the account password on XML-RPC and REST for anyone with a second 40 * factor. 41 * 42 * Those requests never reach wp_login, where the challenge runs, so a 43 * password accepted there is a sign-in with one factor. Application 44 * passwords are the way in for API clients and are left alone. 45 * 46 * @param mixed $user 47 * @return mixed 48 */ 49 public function refuse_api_password( $user ) { 50 if ( ! $user instanceof \WP_User || ! self::is_api_request() ) { 51 return $user; 52 } 53 54 if ( isset( $this->app_password_users[ (int) $user->ID ] ) ) { 55 return $user; 56 } 57 58 if ( ! Providers::instance()->has_usable( (int) $user->ID ) ) { 59 return $user; 60 } 61 62 return new \WP_Error( 63 'sigil_api_password_refused', 64 __( 'This account uses two-factor authentication. Use an application password for API access.', 'sigil-2fa' ) 65 ); 66 } 67 68 public static function is_api_request(): bool { 69 return ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ); 22 70 } 23 71 -
sigil-2fa/trunk/includes/class-cli.php
r3634087 r3752373 146 146 147 147 /** 148 * Import users' two-factor methods from another plugin. 149 * 150 * Reads the source plugin's data and never changes or deletes it. Anything 151 * already set up in Sigil is left alone, so it is safe to run again. 152 * 153 * ## OPTIONS 154 * 155 * <source> 156 * : Plugin to import from. 157 * --- 158 * options: 159 * - two-factor 160 * --- 161 * 162 * [--dry-run] 163 * : Report what would be imported without writing anything. 164 * 165 * [--user=<user>] 166 * : Only this user (ID, login, or email). 167 * 168 * ## EXAMPLES 169 * 170 * wp sigil import two-factor --dry-run 171 * wp sigil import two-factor 172 * wp sigil import two-factor --user=12 173 * 174 * @param list<string> $args 175 * @param array<string, string|bool> $assoc_args 176 */ 177 public function import( array $args, array $assoc_args = [] ): void { 178 if ( 'two-factor' !== ( $args[0] ?? '' ) ) { 179 \WP_CLI::error( 'Unknown source. Supported: two-factor' ); 180 } 181 182 $dry_run = ! empty( $assoc_args['dry-run'] ); 183 $ids = null; 184 185 if ( isset( $assoc_args['user'] ) ) { 186 $user = $this->resolve_user( (string) $assoc_args['user'] ); 187 if ( null === $user ) { 188 \WP_CLI::error( 'User not found.' ); 189 } 190 $ids = [ (int) $user->ID ]; 191 } 192 193 $report = Two_Factor_Import::run( $ids, $dry_run ); 194 195 $items = []; 196 foreach ( $report['users'] as $row ) { 197 $items[] = [ 198 'ID' => $row['user_id'], 199 'login' => $row['login'], 200 'status' => $row['status'], 201 'imported' => implode( ',', $row['imported'] ), 202 'already' => implode( ',', $row['already'] ), 203 'preferred' => $row['preferred'], 204 'flags' => implode( ',', $row['flags'] ), 205 ]; 206 } 207 208 if ( [] !== $items ) { 209 \WP_CLI\Utils\format_items( 'table', $items, [ 'ID', 'login', 'status', 'imported', 'already', 'preferred', 'flags' ] ); 210 } 211 212 $c = $report['counts']; 213 \WP_CLI::log( 214 sprintf( 215 '%sUsers: %d, not using Two Factor: %d, email: %d, authenticator: %d, backup codes: %d, already in Sigil: %d, flagged: %d, needs attention: %d', 216 $dry_run ? '[dry run] ' : '', 217 $c['users'], 218 $c['not_using'], 219 $c['email'], 220 $c['totp'], 221 $c['backup'], 222 $c['already'], 223 $c['flagged'], 224 $c['attention'] 225 ) 226 ); 227 228 if ( $c['attention'] > 0 ) { 229 \WP_CLI::warning( 'Some users had a method Sigil could not carry over. Two Factor keeps protecting them while it is active, so leave it active until they have set up a method in Sigil.' ); 230 return; 231 } 232 233 \WP_CLI::success( $dry_run ? 'Dry run complete. Nothing was written.' : 'Import complete. Two Factor data was left in place; deactivate Two Factor once you have checked the result.' ); 234 } 235 236 /** 148 237 * @return list<string> 149 238 */ -
sigil-2fa/trunk/includes/class-plugin.php
r3635650 r3752373 67 67 Frontend::instance(); 68 68 69 require_once SIGIL_DIR . 'includes/class-two-factor-import.php'; 70 require_once SIGIL_DIR . 'includes/class-two-factor-migration.php'; 71 Two_Factor_Migration::instance(); 72 69 73 // Pro add-on, when present. The free build strips pro/ entirely, so this 70 74 // include only fires in the Pro distribution. -
sigil-2fa/trunk/readme.txt
r3635650 r3752373 3 3 Tags: two-factor, 2fa, passkeys, authentication, security 4 4 Requires at least: 6.9 5 Tested up to: 7. 05 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0. 3.37 Stable tag: 0.4.0 8 8 License: GPL-3.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 83 83 = Does this work with application passwords, the REST API, and XML-RPC? = 84 84 85 Application passwords bypass 2FA by design, as that is how WordPress authenticates automated requests. The settings screen documents this, and application passwords can be disabled per role to close that path. 85 Application passwords bypass 2FA by design, as that is how WordPress authenticates automated requests. The settings screen documents this, and application passwords can be disabled per role to close that path. The account password itself is refused on XML-RPC and REST for anyone with a second factor, because those requests never reach the challenge. 86 86 87 87 = Which PHP version do I need for passkeys? = … … 97 97 Yes. Put `[sigil_2fa]` on a page. Users manage their methods from there, and anyone required to enrol is sent to that page rather than to the dashboard. 98 98 99 = Can I switch from the Two Factor plugin? = 100 101 Yes. With both plugins active, open Settings → Sigil and use Import from Two Factor, or run `wp sigil import two-factor` (add `--dry-run` to preview). Email codes, authenticator apps and backup codes come across, so people keep signing in with the same app. Anyone with a method that cannot be carried over is listed in the report, and Two Factor keeps protecting them while it stays active. Two Factor's own data is not changed. Once everyone on that list has set up a method in Sigil, deactivate Two Factor. 102 99 103 = Can I enforce 2FA only for administrators? = 100 104 … … 102 106 103 107 == Changelog == 108 109 = 0.4.0 = 110 * Moving from the Two Factor plugin no longer means everyone enrols again. Settings → Sigil and `wp sigil import two-factor` carry each user's email codes, authenticator app and backup codes across, so the same authenticator keeps working. Anyone with a method that cannot be carried over is named in the report and stays with Two Factor until they set one up in Sigil. Nobody is moved to email codes they did not already use, and Two Factor's own data is left as it was. 111 * While both plugins are active, Two Factor stands down for anyone Sigil already covers, so nobody is asked for a code twice. A notice offers the import until Two Factor is deactivated. 112 * Security: XML-RPC and REST no longer accept the account password from someone with a second factor. Those requests never reach the two-factor challenge, so a password alone was enough there. Application passwords still work. 104 113 105 114 = 0.3.3 = -
sigil-2fa/trunk/sigil-2fa.php
r3635650 r3752373 3 3 * Plugin Name: Sigil – Passkeys and Two-Factor Authentication 4 4 * Description: Two-factor authentication for WordPress logins: passkeys, authenticator apps, backup codes and email codes, with per-role enforcement, multisite support and account recovery. 5 * Version: 0. 3.35 * Version: 0.4.0 6 6 * Requires at least: 6.9 7 7 * Requires PHP: 7.4 … … 18 18 defined( 'ABSPATH' ) || exit; 19 19 20 define( 'SIGIL_VERSION', '0. 3.3' );20 define( 'SIGIL_VERSION', '0.4.0' ); 21 21 define( 'SIGIL_FILE', __FILE__ ); 22 22 define( 'SIGIL_DIR', plugin_dir_path( __FILE__ ) );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)