diff --git a/common/includes/wporg-sso/wp-plugin.php b/common/includes/wporg-sso/wp-plugin.php index 178311f285..8bb356a416 100644 --- a/common/includes/wporg-sso/wp-plugin.php +++ b/common/includes/wporg-sso/wp-plugin.php @@ -29,6 +29,7 @@ class WP_WPOrg_SSO extends WPOrg_SSO { // Primarily for logged in users. 'updated-tos' => '/updated-policies', 'enable-2fa' => '/enable-2fa', + 'backup-codes' => '/backup-codes', 'logout' => '/logout', // Primarily for logged out users. @@ -55,6 +56,13 @@ class WP_WPOrg_SSO extends WPOrg_SSO { */ static $matched_route_params = array(); + /** + * Holds the last set auth cookie. + * + * @var array + */ + protected $last_auth_cookie = array(); + /** * Constructor: add our action(s)/filter(s) */ @@ -98,12 +106,29 @@ public function __construct() { // Updated TOS interceptor. add_filter( 'send_auth_cookies', [ $this, 'maybe_block_auth_cookies' ], 100, 5 ); + // See https://core.trac.wordpress.org/ticket/61874 + add_action( 'set_auth_cookie', [ $this, 'record_last_auth_cookie' ], 10, 6 ); + // Maybe nag about 2FA - add_filter( 'login_redirect', [ $this, 'maybe_redirect_to_enable_2fa' ], 1000, 3 ); + add_filter( 'login_redirect', [ $this, 'maybe_redirect_to_backup_codes' ], 500, 3 ); + add_filter( 'login_redirect', [ $this, 'maybe_redirect_to_enable_2fa' ], 1100, 3 ); } } } + /** + * Records the last set cookies, because WordPress. + * + * During the WordPress login process, the authentication cookies are not yet available, + * but we need to know the user token (contained in those cookies) to retrieve their session. + * To work around this, we store the set authentication cookies here for later usage. + * + * @see https://core.trac.wordpress.org/ticket/61874 + */ + function record_last_auth_cookie( $auth_cookie, $expire, $expiration, $user_id, $scheme, $token ) { + $this->last_auth_cookie = compact( 'auth_cookie', 'expire', 'expiration', 'user_id', 'scheme', 'token' ); + } + /** * Inherits the 'registration' option from the main network. * @@ -851,6 +876,53 @@ public function maybe_redirect_to_enable_2fa( $redirect, $orig_redirect, $user ) ); } + /** + * Redirects the user to the 2FA Backup codes nag if needed. + */ + public function maybe_redirect_to_backup_codes( $redirect, $orig_redirect, $user ) { + if ( + // No valid user. + is_wp_error( $user ) || + // Or we're already going there. + str_contains( $redirect, '/backup-codes' ) || + // Or the user doesn't use 2FA + ! Two_Factor_Core::is_user_using_two_factor( $user->ID ) + ) { + // Then we don't need to redirect to the enable 2FA page. + return $redirect; + } + + // If the user logged in with a backup code.. + $session_token = wp_get_session_token() ?: ( $this->last_auth_cookie['token'] ?? '' ); + $session = WP_Session_Tokens::get_instance( $user->ID )->get( $session_token ); + $used_backup_code = str_contains( $session['two-factor-provider'] ?? '', 'Backup_Codes' ); + $codes_available = Two_Factor_Backup_Codes::codes_remaining_for_user( $user ); + + if ( + // If they didn't use a backup code, + ! $used_backup_code && + ( + // They have ample codes available.. + $codes_available > 3 || + // or they've already been nagged about only having a few left (and actually have them) + ( + $codes_available && + $codes_available >= (int) get_user_meta( $user->ID, 'last_2fa_backup_codes_nag', true ) + ) + ) + ) { + // No need to nag. + return $redirect; + } + + // Redirect to the Backup Codes nag. + return add_query_arg( + 'redirect_to', + urlencode( $redirect ), + home_url( '/backup-codes' ) + ); + } + /** * Whether the given user_id has agreed to the current version of the TOS. */ diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/backup-codes.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/backup-codes.php new file mode 100644 index 0000000000..949391b147 --- /dev/null +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/backup-codes.php @@ -0,0 +1,88 @@ +ID )->get( wp_get_session_token() ); +$used_backup_code = str_contains( $session['two-factor-provider'] ?? '', 'Backup_Codes' ); +$codes_available = Two_Factor_Backup_Codes::codes_remaining_for_user( $user ); +$can_ignore = ! $used_backup_code || ( $used_backup_code && $codes_available > 1 ); + +if ( isset( $_REQUEST['redirect_to'] ) ) { + $redirect_to = wp_validate_redirect( wp_unslash( $_REQUEST['redirect_to'] ), $redirect_to ); +} + +// If the user is here in error, redirect off. +if ( ! is_user_logged_in() || ! Two_Factor_Core::is_user_using_two_factor( $user->ID ) ) { + wp_safe_redirect( $redirect_to ); + exit; +} + +/** + * Record the last time we nagged the user about backup codes, as we only want to do this once per code-use. + */ +update_user_meta( $user->ID, 'last_2fa_backup_codes_nag', $codes_available ); + +get_header(); +?> + +
+ ++ +
These codes are intended to be used when you lose access to your authentication device.
Please take a moment to review your account settings and ensure your two-factor settings are up-to-date.", 'wporg-login' );
+ } else {
+ if ( ! $codes_available ) {
+ _e( 'You do not have any backup codes remaining.', 'wporg-login' );
+ } else {
+ printf(
+ _n(
+ 'You have %s backup code remaining.',
+ 'You have %s backup codes remaining.',
+ $codes_available,
+ 'wporg-login'
+ ),
+ '' . number_format_i18n( $codes_available ) . '
'
+ );
+ }
+
+ // Direct to the backup codes screen.
+ $account_settings_url = add_query_arg( 'screen', 'backup-codes', $account_settings_url );
+ }
+?>
+ + + +
+ + + + + + + + diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/enable-2fa.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/enable-2fa.php index f0db46d09b..17d5ababbf 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/enable-2fa.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/enable-2fa.php @@ -9,7 +9,16 @@ $user = wp_get_current_user(); $requires_2fa = user_requires_2fa( $user ); $should_2fa = user_should_2fa( $user ); // If they're on this page, this should be truthful. -$redirect_to = wp_validate_redirect( wp_unslash( $_REQUEST['redirect_to'] ?? '' ), wporg_login_wordpress_url() ); +$redirect_to = wporg_login_wordpress_url(); +if ( isset( $_REQUEST['redirect_to'] ) ) { + $redirect_to = wp_validate_redirect( wp_unslash( $_REQUEST['redirect_to'] ), $redirect_to ); +} + +// If the user is here in error, redirect off. +if ( ! is_user_logged_in() || Two_Factor_Core::is_user_using_two_factor( $user->ID ) ) { + wp_safe_redirect( $redirect_to ); + exit; +} /* * Record the last time we naged the user about 2FA.