Skip to content

Commit

Permalink
Login: 2FA: Add a post-login nag to setup backup codes when either no…
Browse files Browse the repository at this point in the history
…ne are saved or the user is running really low on them.

This will hopefully reduce the number of users who become locked out of their account after losing their authentication key / device / etc.

Merges #358
Fixes WordPress/wporg-two-factor#279
See WordPress/wporg-two-factor#300, WordPress/wporg-two-factor#275


git-svn-id: https://meta.svn.wordpress.org/sites/trunk@13982 74240141-8908-4e6f-9713-ba540dce6ec7
  • Loading branch information
dd32 committed Aug 20, 2024
1 parent 451ff0a commit 9c08d89
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 2 deletions.
74 changes: 73 additions & 1 deletion common/includes/wporg-sso/wp-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
*/
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php
use function WordPressdotorg\Two_Factor\get_edit_account_url;
/**
* The 'Backup Codes' post-login screen.
*
* This template is used for two primary purposes:
* 1. The user has logged in with a backup code, we need to push them to verify their 2FA settings.
* 2. The user is running low on backup codes (or has none!), we need to remind them to generate new ones.
*
* @package wporg-login
*/

$account_settings_url = get_edit_account_url();
$redirect_to = wporg_login_wordpress_url();
$user = wp_get_current_user();
$session = WP_Session_Tokens::get_instance( $user->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();
?>

<h2 class="center"><?php
if ( $used_backup_code ) {
_e( 'Backup Code used', 'wporg-login' );
} else {
_e( 'Account Backup Codes', 'wporg-login' );
}
?></h2>

<p>&nbsp;</p>

<p><?php
if ( $used_backup_code ) {
_e( "You've logged in with a backup code.<br>These codes are intended to be used when you lose access to your authentication device.<br>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'
),
'<code>' . number_format_i18n( $codes_available ) . '</code>'
);
}

// Direct to the backup codes screen.
$account_settings_url = add_query_arg( 'screen', 'backup-codes', $account_settings_url );
}
?></p>

<p>&nbsp;</p>

<p><?php
_e( 'If you run out of backup codes and no longer have access to your authentication device, you are at risk of being locked out of your WordPress.org account if we are unable to verify account ownership.', 'wporg-login' );
?></p>

<p>&nbsp;</p>

<p><a href="<?php echo esc_url( $account_settings_url ); ?>"><button class="button-primary"><?php _e( 'View my account settings', 'wporg-login' ); ?></button></a></p>

<?php if ( $can_ignore ) { ?>
<p id="nav">
<a href="<?php echo esc_url( $redirect_to ); ?>" style="font-style: italic;"><?php _e( "I'll do this later", 'wporg-login' ); ?></a>
</p>
<?php } ?>

<?php get_footer(); ?>
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 9c08d89

Please sign in to comment.