Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebAuthn changes required for production #169

Merged
merged 7 commits into from
May 22, 2023
131 changes: 131 additions & 0 deletions class-wporg-webauthn-provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
namespace WordPressdotorg\Two_Factor;
use TwoFactor_Provider_WebAuthn, Two_Factor_Core;

/**
* Extends the TwoFactor_Provider_WebAuthn class for WordPress.org needs.
*/
class WPORG_TwoFactor_Provider_WebAuthn extends TwoFactor_Provider_WebAuthn {
/**
* Use the parent class as the "key" in the Two Factor UI.
*/
public function get_key() {
return parent::class;
}

/**
* Override the parent constructor to allow for adding filters early.
*
* This would be ideal as a `__construct()` method, but the parent is final and $instance is private.
*/
public static function get_instance() {
static $instance = false;
if ( ! $instance ) {
$instance = new static();
}

// Add the custom filters needed for this class
static $filters_added = false;
if ( ! $filters_added ) {
$filters_added = true;

$instance->_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' );
}

iandunn marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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 `<fieldset>` to wrap the additional providers?
echo '<fieldset disabled="disabled">';
add_action( 'show_user_security_settings', function() { echo '</fieldset>'; }, 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' );
}
}
36 changes: 32 additions & 4 deletions wporg-two-factor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 );
Expand Down Expand Up @@ -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 ) {
Expand Down