diff --git a/class-wporg-webauthn-provider.php b/class-wporg-webauthn-provider.php new file mode 100644 index 00000000..d279321b --- /dev/null +++ b/class-wporg-webauthn-provider.php @@ -0,0 +1,131 @@ +_add_filters(); + } + + return $instance; + } + + /** + * Check if the provider is available for the given user. + * + * This method includes caching for WordPress.org, as it's called on most pageloads. + */ + public function is_available_for_user( $user ) { + $is_available = wp_cache_get( 'webauthn:' . $user->ID, 'users', false, $found ); + if ( $found ) { + return $is_available; + } + + $is_available = parent::is_available_for_user( $user ); + + wp_cache_set( 'webauthn:' . $user->ID, $is_available, 'users', HOUR_IN_SECONDS ); + + return $is_available; + } + + /** + * See https://github.com/sjinks/wp-two-factor-provider-webauthn/pull/468 + * + * @return string + */ + public function get_alternative_provider_label() { + return __( 'Use your security key', 'wporg-two-factor' ); + } + + /** + * Add some filters to watch for WebAuthn events. + */ + public function _add_filters() { + // Clear the cache when a user is updated. + add_action( 'wp_ajax_webauthn_preregister', [ $this, '_webauthn_ajax_request' ], 1 ); + add_action( 'wp_ajax_webauthn_register', [ $this, '_webauthn_ajax_request' ], 1 ); + add_action( 'wp_ajax_webauthn_delete_key', [ $this, '_webauthn_ajax_request' ], 1 ); + add_action( 'wp_ajax_webauthn_rename_key', [ $this, '_webauthn_ajax_request' ], 1 ); + + // Disable the admin UI if it needs revalidation. + add_action( 'show_user_security_settings', [ $this, '_show_user_security_settings' ], -1 ); + + // Extend the session revalidation after registering a new key. + add_action( 'wp_ajax_webauthn_register', [ $this, '_extend_revalidation' ], 1 ); + } + + /** + * Force the user to revalidate their 2FA if they're updating their WebAuthn keys. + * + * This is pending an upstream PR for the revalidation. + */ + public function _webauthn_ajax_request() { + // Check the users session is still active and 2FA revalidation isn't required. + if ( ! Two_Factor_Core::current_user_can_update_two_factor_options() ) { + wp_send_json_error( 'Your session has expired. Please refresh the page and try again.' ); + } + + // Clear the caches for this class after the request is finished. + add_action( 'shutdown', [ $this, '_clear_cache' ] ); + } + + /** + * Wrap the additional providers in a disabled fieldset if the user needs to revalidate. + * + * This is pending an upstream PR. + */ + public function _show_user_security_settings() { + $show_2fa_options = Two_Factor_Core::current_user_can_update_two_factor_options(); + if ( ! $show_2fa_options ) { + // TODO: Perhaps the Core UI should extend it's `
` to wrap the additional providers? + echo '
'; + add_action( 'show_user_security_settings', function() { echo '
'; }, 1001 ); + } + } + + /** + * Extend the session revalidation after registering a new key. + */ + public function _extend_revalidation() { + ob_start( function( $output ) { + $json = json_decode( $output, true ); + if ( ! empty( $json['success'] ) ) { + // Bump session revalidation. + Two_Factor_Core::update_current_user_session( [ + 'two-factor-login' => time(), + ] ); + } + + return $output; + } ); + } + + public function _clear_cache() { + wp_cache_delete( 'webauthn:' . get_current_user_id(), 'users' ); + } +} diff --git a/wporg-two-factor.php b/wporg-two-factor.php index cc470935..1e7db3ac 100644 --- a/wporg-two-factor.php +++ b/wporg-two-factor.php @@ -12,6 +12,7 @@ namespace WordPressdotorg\Two_Factor; use Two_Factor_Core; use WildWolf\WordPress\TwoFactorWebAuthn\Plugin as WebAuthn_Plugin; +use WildWolf\WordPress\TwoFactorWebAuthn\Constants as WebAuthn_Plugin_Constants; use WP_User, WP_Error; defined( 'WPINC' ) || die(); @@ -29,15 +30,33 @@ function is_2fa_beta_tester() : bool { require_once __DIR__ . '/settings/settings.php'; -/* +/** + * Load the WebAuthn plugin. + * * Make sure the WebAuthn plugin loads early, because all of our functions that call * `Two_Factor_Core::is_user_using_two_factor()` etc assume that all providers are loaded. If WebAuthn is loaded * too late, then `remove_capabilities_until_2fa_enabled()` would cause `get_enable_2fa_notice()` to be shown on * the front end if WebAuthn is enabled and TOTP isn't. */ -$webauthn = WebAuthn_Plugin::instance(); -$webauthn->init(); -$webauthn->maybe_update_schema(); // This needs to run before plugins_loaded, as jetpack and wporg-two-factor do things way too early to the $current_user. +function load_webauthn_plugin() { + global $wpdb; + + $webauthn = WebAuthn_Plugin::instance(); + $webauthn->init(); + + // Use central WebAuthn tables, instead of ones for each site that shares our user tables. + $wpdb->webauthn_credentials = 'wporg_' . WebAuthn_Plugin_Constants::WA_CREDENTIALS_TABLE_NAME; + $wpdb->webauthn_users = 'wporg_' . WebAuthn_Plugin_Constants::WA_USERS_TABLE_NAME; + + // The schema update checks should not check for updates on every request. + remove_action( 'plugins_loaded', [ $webauthn, 'maybe_update_schema' ] ); + + // The schema update checks do need occur, but only on admin requests on the main network. + if ( 'wporg_' === $wpdb->base_prefix || 'local' === wp_get_environment_type() ) { + add_action( 'admin_init', [ $webauthn, 'maybe_update_schema' ] ); + } +} +load_webauthn_plugin(); add_filter( 'two_factor_providers', __NAMESPACE__ . '\two_factor_providers', 99 ); // Must run _after_ all other plugins. add_filter( 'two_factor_primary_provider_for_user', __NAMESPACE__ . '\set_primary_provider_for_user', 10, 2 ); @@ -280,6 +299,15 @@ function get_edit_account_url() : string { return __NAMESPACE__ . '\Encrypted_Totp_Provider'; } ); +/* + * Switch out the WebAuthN provider for one that uses a tiny bit of caching. + */ +add_filter( 'two_factor_provider_classname_TwoFactor_Provider_WebAuthn', function( string $provider ) : string { + require_once __DIR__ . '/class-wporg-webauthn-provider.php'; + + return __NAMESPACE__ . '\WPORG_TwoFactor_Provider_WebAuthn'; +} ); + // Temp fix for TOTP QR code being broken, see: https://meta.trac.wordpress.org/timeline?from=2023-02-21T04%3A40%3A07Z&precision=second. // Hotfix for https://github.com/WordPress/gutenberg/pull/48268 add_filter( 'block_type_metadata', function( $metadata ) {