Initial geladen: WP App Portal

This commit is contained in:
2026-04-10 11:32:42 +02:00
parent a772a0ad53
commit fdfd055748
5658 changed files with 1968631 additions and 0 deletions
@@ -0,0 +1,280 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
@@ -0,0 +1,63 @@
<?php
/**
* A compatibility layer for some of the most popular plugins.
*
* @package Two_Factor
*/
/**
* A compatibility layer for some of the most popular plugins.
*
* Should be used with care because ideally we wouldn't need
* any integration specific code for this plugin. Everything should
* be handled through clever use of hooks and best practices.
*
* @since 0.5.0
*/
class Two_Factor_Compat {
/**
* Initialize all the custom hooks as necessary.
*
* @since 0.5.0
*
* @return void
*/
public function init() {
/**
* Jetpack
*
* @see https://wordpress.org/plugins/jetpack/
*/
add_filter( 'two_factor_rememberme', array( $this, 'jetpack_rememberme' ) );
}
/**
* Jetpack single sign-on wants long-lived sessions for users.
*
* @since 0.5.0
*
* @param boolean $rememberme Current state of the "remember me" toggle.
*
* @return boolean
*/
public function jetpack_rememberme( $rememberme ) {
$action = filter_input( INPUT_GET, 'action', FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
if ( 'jetpack-sso' === $action && $this->jetpack_is_sso_active() ) {
return true;
}
return $rememberme;
}
/**
* Helper to detect the presence of the active SSO module.
*
* @since 0.5.0
*
* @return boolean
*/
public function jetpack_is_sso_active() {
return ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'sso' ) );
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
<?php
/**
* Extracted from wp-login.php since that file also loads WP core which we already have.
*/
/**
* Outputs the footer for the login page.
*
* @since 3.1.0
*
* @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
* upon successful login.
*
* @param string $input_id Which input to auto-focus.
*/
function login_footer( $input_id = '' ) {
global $interim_login;
// Don't allow interim logins to navigate away from the page.
if ( ! $interim_login ) {
?>
<p id="backtoblog">
<?php
$html_link = sprintf(
'<a href="%s">%s</a>',
esc_url( home_url( '/' ) ),
sprintf(
/* translators: %s: Site title. */
_x( '&larr; Go to %s', 'site' ),
get_bloginfo( 'title', 'display' )
)
);
/**
* Filter the "Go to site" link displayed in the login page footer.
*
* @since 5.7.0
*
* @param string $link HTML link to the home URL of the current site.
*/
echo apply_filters( 'login_site_html_link', $html_link );
?>
</p>
<?php
the_privacy_policy_link( '<div class="privacy-policy-page-link">', '</div>' );
}
?>
</div><?php // End of <div id="login">. ?>
<?php
if ( ! empty( $input_id ) ) {
?>
<script type="text/javascript">
try{document.getElementById('<?php echo $input_id; ?>').focus();}catch(e){}
if(typeof wpOnload==='function')wpOnload();
</script>
<?php
}
/**
* Fires in the login page footer.
*
* @since 3.1.0
*/
do_action( 'login_footer' );
?>
<div class="clear"></div>
</body>
</html>
<?php
}
/**
* Outputs the JavaScript to handle the form shaking on the login page.
*
* @since 3.0.0
*/
function wp_shake_js() {
?>
<script type="text/javascript">
document.querySelector('form').classList.add('shake');
</script>
<?php
}
@@ -0,0 +1,259 @@
<?php
/**
* Extracted from wp-login.php since that file also loads WP core which we already have.
*/
/**
* Output the login page header.
*
* @since 2.1.0
*
* @global string $error Login error message set by deprecated pluggable wp_login() function
* or plugins replacing it.
* @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
* upon successful login.
* @global string $action The action that brought the visitor to the login page.
*
* @param string $title Optional. WordPress login Page title to display in the `<title>` element.
* Default 'Log In'.
* @param string $message Optional. Message to display in header. Default empty.
* @param WP_Error $wp_error Optional. The error to pass. Default is a WP_Error instance.
*/
function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
global $error, $interim_login, $action;
// Don't index any of these forms.
add_filter( 'wp_robots', 'wp_robots_sensitive_page' );
add_action( 'login_head', 'wp_strict_cross_origin_referrer' );
add_action( 'login_head', 'wp_login_viewport_meta' );
if ( ! is_wp_error( $wp_error ) ) {
$wp_error = new WP_Error();
}
// Shake it!
$shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password', 'retrieve_password_email_failure' );
/**
* Filters the error codes array for shaking the login form.
*
* @since 3.0.0
*
* @param array $shake_error_codes Error codes that shake the login form.
*/
$shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes );
if ( $shake_error_codes && $wp_error->has_errors() && in_array( $wp_error->get_error_code(), $shake_error_codes, true ) ) {
add_action( 'login_footer', 'wp_shake_js', 12 );
}
$login_title = get_bloginfo( 'name', 'display' );
/* translators: Login screen title. 1: Login screen name, 2: Network or site name. */
$login_title = sprintf( __( '%1$s &lsaquo; %2$s &#8212; WordPress' ), $title, $login_title );
if ( wp_is_recovery_mode() ) {
/* translators: %s: Login screen title. */
$login_title = sprintf( __( 'Recovery Mode &#8212; %s' ), $login_title );
}
/**
* Filters the title tag content for login page.
*
* @since 4.9.0
*
* @param string $login_title The page title, with extra context added.
* @param string $title The original page title.
*/
$login_title = apply_filters( 'login_title', $login_title, $title );
?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>" />
<title><?php echo $login_title; ?></title>
<?php
wp_enqueue_style( 'login' );
/*
* Remove all stored post data on logging out.
* This could be added by add_action('login_head'...) like wp_shake_js(),
* but maybe better if it's not removable by plugins.
*/
if ( 'loggedout' === $wp_error->get_error_code() ) {
?>
<script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script>
<?php
}
/**
* Enqueue scripts and styles for the login page.
*
* @since 3.1.0
*/
do_action( 'login_enqueue_scripts' );
/**
* Fires in the login page header after scripts are enqueued.
*
* @since 2.1.0
*/
do_action( 'login_head' );
$login_header_url = __( 'https://wordpress.org/' );
/**
* Filters link URL of the header logo above login form.
*
* @since 2.1.0
*
* @param string $login_header_url Login header logo URL.
*/
$login_header_url = apply_filters( 'login_headerurl', $login_header_url );
$login_header_title = '';
/**
* Filters the title attribute of the header logo above login form.
*
* @since 2.1.0
* @deprecated 5.2.0 Use {@see 'login_headertext'} instead.
*
* @param string $login_header_title Login header logo title attribute.
*/
$login_header_title = apply_filters_deprecated(
'login_headertitle',
array( $login_header_title ),
'5.2.0',
'login_headertext',
__( 'Usage of the title attribute on the login logo is not recommended for accessibility reasons. Use the link text instead.' )
);
$login_header_text = empty( $login_header_title ) ? __( 'Powered by WordPress' ) : $login_header_title;
/**
* Filters the link text of the header logo above the login form.
*
* @since 5.2.0
*
* @param string $login_header_text The login header logo link text.
*/
$login_header_text = apply_filters( 'login_headertext', $login_header_text );
$classes = array( 'login-action-' . $action, 'wp-core-ui' );
if ( is_rtl() ) {
$classes[] = 'rtl';
}
if ( $interim_login ) {
$classes[] = 'interim-login';
?>
<style type="text/css">html{background-color: transparent;}</style>
<?php
if ( 'success' === $interim_login ) {
$classes[] = 'interim-login-success';
}
}
$classes[] = ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) );
/**
* Filters the login page body classes.
*
* @since 3.5.0
*
* @param array $classes An array of body classes.
* @param string $action The action that brought the visitor to the login page.
*/
$classes = apply_filters( 'login_body_class', $classes, $action );
?>
</head>
<body class="login no-js <?php echo esc_attr( implode( ' ', $classes ) ); ?>">
<script type="text/javascript">
document.body.className = document.body.className.replace('no-js','js');
</script>
<?php
/**
* Fires in the login page header after the body tag is opened.
*
* @since 4.6.0
*/
do_action( 'login_header' );
?>
<div id="login">
<h1><a href="<?php echo esc_url( $login_header_url ); ?>"><?php echo $login_header_text; ?></a></h1>
<?php
/**
* Filters the message to display above the login form.
*
* @since 2.1.0
*
* @param string $message Login message text.
*/
$message = apply_filters( 'login_message', $message );
if ( ! empty( $message ) ) {
echo $message . "\n";
}
// In case a plugin uses $error rather than the $wp_errors object.
if ( ! empty( $error ) ) {
$wp_error->add( 'error', $error );
unset( $error );
}
if ( $wp_error->has_errors() ) {
$errors = '';
$messages = '';
foreach ( $wp_error->get_error_codes() as $code ) {
$severity = $wp_error->get_error_data( $code );
foreach ( $wp_error->get_error_messages( $code ) as $error_message ) {
if ( 'message' === $severity ) {
$messages .= ' ' . $error_message . "<br />\n";
} else {
$errors .= ' ' . $error_message . "<br />\n";
}
}
}
if ( ! empty( $errors ) ) {
/**
* Filters the error messages displayed above the login form.
*
* @since 2.1.0
*
* @param string $errors Login error message.
*/
echo '<div id="login_error">' . apply_filters( 'login_errors', $errors ) . "</div>\n";
}
if ( ! empty( $messages ) ) {
/**
* Filters instructional messages displayed above the login form.
*
* @since 2.5.0
*
* @param string $messages Login messages.
*/
echo '<p class="message">' . apply_filters( 'login_messages', $messages ) . "</p>\n";
}
}
} // End of login_header().
/**
* Outputs the viewport meta tag for the login page.
*
* @since 3.7.0
*/
function wp_login_viewport_meta() {
?>
<meta name="viewport" content="width=device-width" />
<?php
}
@@ -0,0 +1,486 @@
<?php
/**
* Class for creating a backup codes provider.
*
* @package Two_Factor
*/
/**
* Class for creating a backup codes provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_Backup_Codes extends Two_Factor_Provider {
/**
* The user meta backup codes key.
*
* @type string
*/
const BACKUP_CODES_META_KEY = '_two_factor_backup_codes';
/**
* The number backup codes.
*
* @type int
*/
const NUMBER_OF_CODES = 10;
/**
* Class constructor.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
protected function __construct() {
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
parent::__construct();
}
/**
* Enqueue scripts for backup codes.
*
* @since 0.10.0
*
* @codeCoverageIgnore
*
* @param string $hook_suffix Optional. The current admin page hook suffix.
*/
public function enqueue_assets( $hook_suffix = '' ) {
wp_register_script(
'two-factor-backup-codes-admin',
plugins_url( 'js/backup-codes-admin.js', __FILE__ ),
array( 'jquery', 'wp-api-request' ),
TWO_FACTOR_VERSION,
true
);
}
/**
* Register the rest-api endpoints required for this provider.
*
* @since 0.8.0
*
* @codeCoverageIgnore
*/
public function register_rest_routes() {
register_rest_route(
Two_Factor_Core::REST_NAMESPACE,
'/generate-backup-codes',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'rest_generate_codes' ),
'permission_callback' => function ( $request ) {
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
},
'args' => array(
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'enable_provider' => array(
'required' => false,
'type' => 'boolean',
'default' => false,
),
),
)
);
}
/**
* Displays an admin notice when backup codes have run out.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
public function admin_notices() {
$user = wp_get_current_user();
// Return if the provider is not enabled.
if ( ! in_array( __CLASS__, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ), true ) ) {
return;
}
// Return if we are not out of codes.
if ( $this->is_available_for_user( $user ) ) {
return;
}
?>
<div class="error">
<p>
<span>
<?php
echo wp_kses(
sprintf(
/* translators: %s: URL for code regeneration */
__( 'Two-Factor: You are out of recovery codes and need to <a href="%s">regenerate!</a>', 'two-factor' ),
esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' )
),
array( 'a' => array( 'href' => true ) )
);
?>
</span>
</p>
</div>
<?php
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'Recovery Codes', 'Provider Label', 'two-factor' );
}
/**
* Returns the "continue with" text provider for the login screen.
*
* @since 0.9.0
*/
public function get_alternative_provider_label() {
return __( 'Use a recovery code', 'two-factor' );
}
/**
* Whether this Two Factor provider is configured and codes are available for the user specified.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function is_available_for_user( $user ) {
// Does this user have available codes?
if ( 0 < self::codes_remaining_for_user( $user ) ) {
return true;
}
return false;
}
/**
* Inserts markup at the end of the user profile field for this provider.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function user_options( $user ) {
wp_localize_script(
'two-factor-backup-codes-admin',
'twoFactorBackupCodes',
array(
'restPath' => Two_Factor_Core::REST_NAMESPACE . '/generate-backup-codes',
'userId' => $user->ID,
)
);
wp_enqueue_script( 'two-factor-backup-codes-admin' );
$count = self::codes_remaining_for_user( $user );
?>
<div id="two-factor-backup-codes">
<p class="two-factor-backup-codes-count">
<?php
echo esc_html(
sprintf(
/* translators: %s: count */
_n( '%s unused code remaining, each recovery code can only be used once.', '%s unused codes remaining, each recovery code can only be used once.', $count, 'two-factor' ),
$count
)
);
?>
</p>
<p>
<button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
<?php esc_html_e( 'Generate new recovery codes', 'two-factor' ); ?>
</button>
<em><?php esc_html_e( 'This invalidates all currently stored codes.', 'two-factor' ); ?></em>
</p>
</div>
<div class="two-factor-backup-codes-wrapper" style="display:none;">
<div class="two-factor-backup-codes-list-wrap">
<ol class="two-factor-backup-codes-unused-codes"></ol>
</div>
<p class="description"><?php esc_html_e( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'two-factor' ); ?></p>
<p>
<a class="button button-two-factor-backup-codes-copy button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-copy-link"><?php esc_html_e( 'Copy Codes', 'two-factor' ); ?></a>
<a class="button button-two-factor-backup-codes-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'two-factor' ); ?></a>
</p>
</div>
<?php
}
/**
* Get the backup code length for a user.
*
* @since 0.11.0
*
* @param WP_User $user User object.
*
* @return int Number of characters.
*/
private function get_backup_code_length( $user ) {
/**
* Filters the character count of the backup codes.
*
* @since 0.11.0
*
* @param int $code_length Length of the backup code. Default 8.
* @param WP_User $user User object.
*/
$code_length = (int) apply_filters( 'two_factor_backup_code_length', 8, $user );
return $code_length;
}
/**
* Generates backup codes & updates the user meta.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @param array $args Optional arguments for assigning new codes.
* @return array
*/
public function generate_codes( $user, $args = array() ) {
$codes = array();
$codes_hashed = array();
// Check for arguments.
if ( isset( $args['number'] ) ) {
$num_codes = (int) $args['number'];
} else {
$num_codes = self::NUMBER_OF_CODES;
}
// Append or replace (default).
if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
$codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
}
$code_length = $this->get_backup_code_length( $user );
for ( $i = 0; $i < $num_codes; $i++ ) {
$code = $this->get_code( $code_length );
$codes_hashed[] = wp_hash_password( $code );
$codes[] = $code;
unset( $code );
}
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed );
// Unhashed.
return $codes;
}
/**
* Generates Backup Codes for returning through the WordPress Rest API.
*
* @since 0.8.0
* @param WP_REST_Request $request Request object.
* @return array|WP_Error
*/
public function rest_generate_codes( $request ) {
$user_id = $request['user_id'];
$user = get_user_by( 'id', $user_id );
// Hardcode these, the user shouldn't be able to choose them.
$args = array(
'number' => self::NUMBER_OF_CODES,
'method' => 'replace',
);
// Setup the return data.
$codes = $this->generate_codes( $user, $args );
$count = self::codes_remaining_for_user( $user );
$title = sprintf(
/* translators: %s: the site's domain */
__( 'Two-Factor Recovery Codes for %s', 'two-factor' ),
home_url( '/' )
);
// Generate download content.
$download_link = 'data:application/text;charset=utf-8,';
$download_link .= rawurlencode( "{$title}\r\n\r\n" );
$i = 1;
foreach ( $codes as $code ) {
$download_link .= rawurlencode( "{$i}. {$code}\r\n" );
++$i;
}
$i18n = array(
/* translators: %s: count */
'count' => esc_html( sprintf( _n( '%s unused code remaining, each recovery code can only be used once.', '%s unused codes remaining, each recovery code can only be used once.', $count, 'two-factor' ), $count ) ),
);
if ( $request->get_param( 'enable_provider' ) && ! Two_Factor_Core::enable_provider_for_user( $user_id, 'Two_Factor_Backup_Codes' ) ) {
return new WP_Error( 'db_error', __( 'Unable to enable recovery codes for this user.', 'two-factor' ), array( 'status' => 500 ) );
}
return array(
'codes' => $codes,
'download_link' => $download_link,
'remaining' => $count,
'i18n' => $i18n,
);
}
/**
* Returns the number of unused codes for the specified user
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @return int $int The number of unused codes remaining
*/
public static function codes_remaining_for_user( $user ) {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
return count( $backup_codes );
}
return 0;
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
$code_length = $this->get_backup_code_length( $user );
$code_placeholder = str_repeat( 'X', $code_length );
?>
<?php
/**
* Fires before the two-factor authentication prompt text.
*
* @since 0.15.0
*
* @param Two_Factor_Provider $provider The two-factor provider instance.
*/
do_action( 'two_factor_before_authentication_prompt', $this );
?>
<p class="two-factor-prompt"><?php esc_html_e( 'Enter a recovery code.', 'two-factor' ); ?></p>
<?php
/**
* Fires after the two-factor authentication prompt text.
*
* @since 0.15.0
*
* @param Two_Factor_Provider $provider The two-factor provider instance.
*/
do_action( 'two_factor_after_authentication_prompt', $this );
?>
<p>
<label for="authcode"><?php esc_html_e( 'Recovery Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" name="two-factor-backup-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $code_placeholder ); ?>" data-digits="<?php echo esc_attr( $code_length ); ?>" />
</p>
<?php
/**
* Fires after the two-factor authentication input field.
*
* @since 0.15.0
*
* @param Two_Factor_Provider $provider The two-factor provider instance.
*/
do_action( 'two_factor_after_authentication_input', $this );
?>
<?php
submit_button( __( 'Verify', 'two-factor' ) );
}
/**
* Validates the users input token.
*
* In this class we just return true.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
$backup_code = $this->sanitize_code_from_request( 'two-factor-backup-code' );
if ( ! $backup_code ) {
return false;
}
return $this->validate_code( $user, $backup_code );
}
/**
* Validates a backup code.
*
* Backup Codes are single use and are deleted upon a successful validation.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @param int $code The backup code.
* @return boolean
*/
public function validate_code( $user, $code ) {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
foreach ( $backup_codes as $code_index => $code_hashed ) {
if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
$this->delete_code( $user, $code_hashed );
return true;
}
}
}
return false;
}
/**
* Deletes a backup code.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @param string $code_hashed The hashed the backup code.
*/
public function delete_code( $user, $code_hashed ) {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
// Delete the current code from the list since it's been used.
$backup_codes = array_flip( $backup_codes );
unset( $backup_codes[ $code_hashed ] );
$backup_codes = array_values( array_flip( $backup_codes ) );
// Update the backup code master list.
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes );
}
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {
return array(
self::BACKUP_CODES_META_KEY,
);
}
}
@@ -0,0 +1,97 @@
<?php
/**
* Class for creating a dummy provider.
*
* @package Two_Factor
*/
/**
* Class for creating a dummy provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_Dummy extends Two_Factor_Provider {
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
parent::__construct();
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'Dummy Method', 'Provider Label', 'two-factor' );
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_before_authentication_prompt', $this );
?>
<p><?php esc_html_e( 'Are you really you?', 'two-factor' ); ?></p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_prompt', $this );
?>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_input', $this );
?>
<?php
submit_button( __( 'Yup.', 'two-factor' ) );
}
/**
* Validates the users input token.
*
* In this class we just return true.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
return true;
}
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function is_available_for_user( $user ) {
return true;
}
/**
* Inserts markup at the end of the user profile field for this provider.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function user_options( $user ) {}
}
@@ -0,0 +1,465 @@
<?php
/**
* Class for creating an email provider.
*
* @package Two_Factor
*/
/**
* Class for creating an email provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_Email extends Two_Factor_Provider {
/**
* The user meta token key.
*
* @var string
*/
const TOKEN_META_KEY = '_two_factor_email_token';
/**
* Store the timestamp when the token was generated.
*
* @var string
*/
const TOKEN_META_KEY_TIMESTAMP = '_two_factor_email_token_timestamp';
/**
* Name of the input field used for code resend.
*
* @var string
*/
const INPUT_NAME_RESEND_CODE = 'two-factor-email-code-resend';
/**
* Class constructor.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
protected function __construct() {
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
parent::__construct();
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'Email', 'Provider Label', 'two-factor' );
}
/**
* Returns the "continue with" text provider for the login screen.
*
* @since 0.9.0
*/
public function get_alternative_provider_label() {
return __( 'Send a code to your email', 'two-factor' );
}
/**
* Get the email token length.
*
* @since 0.11.0
*
* @return int Email token string length.
*/
private function get_token_length() {
/**
* Filters the number of characters in the email token.
*
* @since 0.11.0
*
* @param int $token_length Number of characters in the email token. Default 8.
*/
$token_length = (int) apply_filters( 'two_factor_email_token_length', 8 );
return $token_length;
}
/**
* Generate the user token.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @return string
*/
public function generate_token( $user_id ) {
$token = $this->get_code( $this->get_token_length() );
update_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, time() );
update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
return $token;
}
/**
* Check if user has a valid token already.
*
* @since 0.2.0
*
* @param int $user_id User ID.
* @return boolean If user has a valid email token.
*/
public function user_has_token( $user_id ) {
$hashed_token = $this->get_user_token( $user_id );
if ( ! empty( $hashed_token ) ) {
return true;
}
return false;
}
/**
* Has the user token validity timestamp expired.
*
* @since 0.6.0
*
* @param integer $user_id User ID.
*
* @return boolean
*/
public function user_token_has_expired( $user_id ) {
$token_lifetime = $this->user_token_lifetime( $user_id );
$token_ttl = $this->user_token_ttl( $user_id );
// Invalid token lifetime is considered an expired token.
if ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl ) {
return false;
}
return true;
}
/**
* Get the lifetime of a user token in seconds.
*
* @since 0.6.0
*
* @param integer $user_id User ID.
*
* @return integer|null Return `null` if the lifetime can't be measured.
*/
public function user_token_lifetime( $user_id ) {
$timestamp = intval( get_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, true ) );
if ( ! empty( $timestamp ) ) {
return time() - $timestamp;
}
return null;
}
/**
* Return the token time-to-live for a user.
*
* @since 0.6.0
*
* @param integer $user_id User ID.
*
* @return integer
*/
public function user_token_ttl( $user_id ) {
$token_ttl = 15 * MINUTE_IN_SECONDS;
/**
* Filters the number of seconds the email token is considered valid after generation.
*
* @since 0.6.0
* @deprecated 0.11.0 Use {@see 'two_factor_email_token_ttl'} instead.
*
* @param int $token_ttl Token time-to-live in seconds.
* @param int $user_id User ID.
*/
$token_ttl = (int) apply_filters_deprecated( 'two_factor_token_ttl', array( $token_ttl, $user_id ), '0.11.0', 'two_factor_email_token_ttl' );
/**
* Filters the number of seconds the email token is considered valid after generation.
*
* @since 0.11.0
*
* @param int $token_ttl Token time-to-live in seconds.
* @param int $user_id User ID.
*/
return (int) apply_filters( 'two_factor_email_token_ttl', $token_ttl, $user_id );
}
/**
* Get the authentication token for the user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
*
* @return string|boolean User token or `false` if no token found.
*/
public function get_user_token( $user_id ) {
$hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
return $hashed_token;
}
return false;
}
/**
* Validate the user token.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param string $token User token.
* @return boolean
*/
public function validate_token( $user_id, $token ) {
$hashed_token = $this->get_user_token( $user_id );
// Bail if token is empty or it doesn't match.
if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) {
return false;
}
if ( $this->user_token_has_expired( $user_id ) ) {
return false;
}
// Ensure the token can be used only once.
$this->delete_token( $user_id );
return true;
}
/**
* Delete the user token.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
*/
public function delete_token( $user_id ) {
delete_user_meta( $user_id, self::TOKEN_META_KEY );
}
/**
* Get the client IP address for the current request.
*
* @since 0.15.0
*
* Note that the IP address is used only for information purposes
* and is expected to be configured correctly, if behind proxy.
*
* @return string|null
*/
private function get_client_ip() {
if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders -- don't have more reliable option for now.
return preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__REMOTE_ADDR__ -- we're limit the allowed characters.
}
return null;
}
/**
* Generate and email the user token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return bool Whether the email contents were sent successfully.
*/
public function generate_and_email_token( $user ) {
$token = $this->generate_token( $user->ID );
$remote_ip = $this->get_client_ip();
$ttl_minutes = (int) ceil( $this->user_token_ttl( $user->ID ) / MINUTE_IN_SECONDS );
$subject = wp_strip_all_tags(
sprintf(
/* translators: %s: site name */
__( '[%s] Login confirmation code', 'two-factor' ),
wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES )
)
);
$message_parts = array(
__( 'Please complete the login by entering the verification code below:', 'two-factor' ),
$token,
sprintf(
/* translators: %d: number of minutes */
__( 'This code will expire in %d minutes.', 'two-factor' ),
$ttl_minutes
),
sprintf(
/* translators: %1$s: IP address of user, %2$s: user login */
__( 'A user from IP address %1$s has successfully authenticated as %2$s. If this wasn\'t you, please change your password.', 'two-factor' ),
$remote_ip,
$user->user_login
),
);
$message = wp_strip_all_tags( implode( "\n\n", $message_parts ) );
/**
* Filters the token email subject.
*
* @since 0.5.2
*
* @param string $subject The email subject line.
* @param int $user_id The ID of the user.
*/
$subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID );
/**
* Filters the token email message.
*
* @since 0.5.2
*
* @param string $message The email message.
* @param string $token The token.
* @param int $user_id The ID of the user.
*/
$message = apply_filters( 'two_factor_token_email_message', $message, $token, $user->ID );
return wp_mail( $user->user_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function authentication_page( $user ) {
if ( ! $user ) {
return;
}
if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
$this->generate_and_email_token( $user );
}
$token_length = $this->get_token_length();
$token_placeholder = str_repeat( 'X', $token_length );
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_before_authentication_prompt', $this );
?>
<p class="two-factor-prompt"><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_prompt', $this );
?>
<p>
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" name="two-factor-email-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" autocomplete="one-time-code" placeholder="<?php echo esc_attr( $token_placeholder ); ?>" data-digits="<?php echo esc_attr( $token_length ); ?>" />
</p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_input', $this );
?>
<?php submit_button( __( 'Verify', 'two-factor' ) ); ?>
<p class="two-factor-email-resend">
<input type="submit" class="button" name="<?php echo esc_attr( self::INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'two-factor' ); ?>" />
</p>
<?php wp_enqueue_script( 'two-factor-login' ); ?>
<?php
}
/**
* Send the email code if missing or requested. Stop the authentication
* validation if a new token has been generated and sent.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function pre_process_authentication( $user ) {
if ( isset( $user->ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- non-distructive option that relies on user state.
$this->generate_and_email_token( $user );
return true;
}
return false;
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
$code = $this->sanitize_code_from_request( 'two-factor-email-code' );
if ( ! isset( $user->ID ) || ! $code ) {
return false;
}
return $this->validate_token( $user->ID, $code );
}
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function is_available_for_user( $user ) {
return true;
}
/**
* Inserts markup at the end of the user profile field for this provider.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function user_options( $user ) {
$email = $user->user_email;
?>
<p>
<?php
echo esc_html(
sprintf(
/* translators: %s: email address */
__( 'Authentication codes will be sent to %s.', 'two-factor' ),
$email
)
);
?>
</p>
<?php
}
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {
return array(
self::TOKEN_META_KEY,
self::TOKEN_META_KEY_TIMESTAMP,
);
}
}
@@ -0,0 +1,214 @@
<?php
/**
* Abstract class for creating two factor authentication providers.
*
* @package Two_Factor
*/
/**
* Abstract class for creating two factor authentication providers.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
abstract class Two_Factor_Provider {
/**
* Ensures only one instance of the provider class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instances = array();
$class_name = static::class;
if ( ! isset( $instances[ $class_name ] ) ) {
$instances[ $class_name ] = new $class_name();
}
return $instances[ $class_name ];
}
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*
* @return string
*/
abstract public function get_label();
/**
* Returns the "continue with" text provider for the login screen.
*
* @since 0.9.0
*
* @return string
*/
public function get_alternative_provider_label() {
return sprintf(
/* translators: the two factor provider name */
__( 'Use %s', 'two-factor' ),
$this->get_label()
);
}
/**
* Prints the name of the provider.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
public function print_label() {
echo esc_html( $this->get_label() );
}
/**
* Retrieves the provider key / slug.
*
* @since 0.9.0
*
* @return string
*/
public function get_key() {
return get_class( $this );
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
abstract public function authentication_page( $user );
/**
* Allow providers to do extra processing before the authentication.
* Return `true` to prevent the authentication and render the
* authentication page.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function pre_process_authentication( $user ) {
return false;
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
abstract public function validate_authentication( $user );
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @since 0.7.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
abstract public function is_available_for_user( $user );
/**
* If this provider should be available for the user.
*
* @since 0.13.0
*
* @param WP_User|int $user WP_User object, user ID or null to resolve the current user.
*
* @return bool
*/
public static function is_supported_for_user( $user = null ) {
$providers = Two_Factor_Core::get_supported_providers_for_user( $user );
return isset( $providers[ static::class ] );
}
/**
* Generate a random eight-digit string to send out as an auth code.
*
* @since 0.1-dev
*
* @param int $length The code length.
* @param string|array $chars Valid auth code characters.
* @return string
*/
public static function get_code( $length = 8, $chars = '1234567890' ) {
$code = '';
if ( is_array( $chars ) ) {
$chars = implode( '', $chars );
}
for ( $i = 0; $i < $length; $i++ ) {
$code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
}
return $code;
}
/**
* Sanitizes a numeric code to be used as an auth code.
*
* @since 0.8.0
*
* @param string $field The _REQUEST field to check for the code.
* @param int $length The valid expected length of the field.
* @return false|string Auth code on success, false if the field is not set or not expected length.
*/
public static function sanitize_code_from_request( $field, $length = 0 ) {
if ( empty( $_REQUEST[ $field ] ) ) {
return false;
}
$code = wp_unslash( $_REQUEST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already.
$code = preg_replace( '/\s+/', '', $code );
// Maybe validate the length.
if ( $length && strlen( $code ) !== $length ) {
return false;
}
return (string) $code;
}
/**
* Return the user meta keys that need to be deletated on plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {
return array();
}
/**
* Return the option keys that need to be deleted on plugin uninstall.
*
* @since 0.10.0
*
* Note: this method doesn't have access to the instantiated provider object.
*
* @return array
*/
public static function uninstall_options() {
return array();
}
}
@@ -0,0 +1,905 @@
<?php
/**
* Class for creating a Time Based One-Time Password provider.
*
* @package Two_Factor
*/
/**
* Class Two_Factor_Totp
*
* @since 0.2.0
*/
class Two_Factor_Totp extends Two_Factor_Provider {
/**
* The user meta key for the TOTP Secret key.
*
* @var string
*/
const SECRET_META_KEY = '_two_factor_totp_key';
/**
* The user meta key for the last successful TOTP token timestamp logged in with.
*
* @var string
*/
const LAST_SUCCESSFUL_LOGIN_META_KEY = '_two_factor_totp_last_successful_login';
const DEFAULT_KEY_BIT_SIZE = 160;
const DEFAULT_CRYPTO = 'sha1';
const DEFAULT_DIGIT_COUNT = 6;
const DEFAULT_TIME_STEP_SEC = 30;
const DEFAULT_TIME_STEP_ALLOWANCE = 4;
/**
* Characters used in base32 encoding.
*
* @var string
*/
private static $base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Class constructor. Sets up hooks, etc.
*
* @since 0.2.0
*
* @codeCoverageIgnore
*/
protected function __construct() {
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_two_factor_options' ) );
parent::__construct();
}
/**
* Timestamp returned by time()
*
* @var int $now
*/
private static $now;
/**
* Override time() in the current object for testing.
*
* @since 0.15.0
*
* @return int
*/
private static function time() {
return self::$now ? self::$now : time();
}
/**
* Set up the internal state of time() invocations for deterministic generation.
*
* @since 0.15.0
*
* @param int $now Timestamp to use when overriding time().
*/
public static function set_time( $now ) {
self::$now = $now;
}
/**
* Register the rest-api endpoints required for this provider.
*
* @since 0.8.0
*
* @codeCoverageIgnore
*/
public function register_rest_routes() {
register_rest_route(
Two_Factor_Core::REST_NAMESPACE,
'/totp',
array(
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'rest_delete_totp' ),
'permission_callback' => function ( $request ) {
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
},
'args' => array(
'user_id' => array(
'required' => true,
'type' => 'integer',
),
),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'rest_setup_totp' ),
'permission_callback' => function ( $request ) {
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
},
'args' => array(
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'key' => array(
'type' => 'string',
'default' => '',
'validate_callback' => null, // Note: validation handled in ::rest_setup_totp().
),
'code' => array(
'type' => 'string',
'default' => '',
'validate_callback' => null, // Note: validation handled in ::rest_setup_totp().
),
'enable_provider' => array(
'required' => false,
'type' => 'boolean',
'default' => false,
),
),
),
)
);
}
/**
* Returns the name of the provider.
*
* @since 0.2.0
*/
public function get_label() {
return _x( 'Authenticator App', 'Provider Label', 'two-factor' );
}
/**
* Returns the "continue with" text provider for the login screen.
*
* @since 0.9.0
*/
public function get_alternative_provider_label() {
return __( 'Use your authenticator app for time-based one-time passwords (TOTP)', 'two-factor' );
}
/**
* Enqueue scripts
*
* @since 0.8.0
*
* @codeCoverageIgnore
* @param string $hook_suffix Hook suffix.
*/
public function enqueue_assets( $hook_suffix ) {
$environment_prefix = file_exists( TWO_FACTOR_DIR . '/dist' ) ? '/dist' : '';
wp_register_script(
'two-factor-qr-code-generator',
plugins_url( $environment_prefix . '/includes/qrcode-generator/qrcode.js', __DIR__ ),
array(),
TWO_FACTOR_VERSION,
true
);
wp_register_script(
'two-factor-totp-qrcode',
plugins_url( 'js/totp-admin-qrcode.js', __FILE__ ),
array( 'two-factor-qr-code-generator' ),
TWO_FACTOR_VERSION,
true
);
wp_register_script(
'two-factor-totp-admin',
plugins_url( 'js/totp-admin.js', __FILE__ ),
array( 'jquery', 'wp-api-request', 'two-factor-qr-code-generator' ),
TWO_FACTOR_VERSION,
true
);
}
/**
* Rest API endpoint for handling deactivation of TOTP.
*
* @since 0.8.0
*
* @param WP_REST_Request $request The Rest Request object.
* @return WP_Error|array Array of data on success, WP_Error on error.
*/
public function rest_delete_totp( $request ) {
$user_id = $request['user_id'];
$user = get_user_by( 'id', $user_id );
if ( ! Two_Factor_Core::disable_provider_for_user( $user_id, 'Two_Factor_Totp' ) ) {
return new WP_Error( 'db_error', __( 'Unable to disable TOTP provider for this user.', 'two-factor' ), array( 'status' => 500 ) );
}
$this->delete_user_totp_key( $user_id );
ob_start();
$this->user_two_factor_options( $user );
$html = ob_get_clean();
return array(
'success' => true,
'html' => $html,
);
}
/**
* REST API endpoint for setting up TOTP.
*
* @since 0.8.0
*
* @param WP_REST_Request $request The Rest Request object.
* @return WP_Error|array Array of data on success, WP_Error on error.
*/
public function rest_setup_totp( $request ) {
$user_id = $request['user_id'];
$user = get_user_by( 'id', $user_id );
$key = $request['key'];
$code = preg_replace( '/\s+/', '', $request['code'] );
if ( ! $this->is_valid_key( $key ) ) {
return new WP_Error( 'invalid_key', __( 'Invalid Two Factor Authentication secret key.', 'two-factor' ), array( 'status' => 400 ) );
}
if ( ! $this->is_valid_authcode( $key, $code ) ) {
return new WP_Error( 'invalid_key_code', __( 'Invalid Two Factor Authentication code.', 'two-factor' ), array( 'status' => 400 ) );
}
if ( ! $this->set_user_totp_key( $user_id, $key ) ) {
return new WP_Error( 'db_error', __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' ), array( 'status' => 500 ) );
}
if ( $request->get_param( 'enable_provider' ) && ! Two_Factor_Core::enable_provider_for_user( $user_id, 'Two_Factor_Totp' ) ) {
return new WP_Error( 'db_error', __( 'Unable to enable TOTP provider for this user.', 'two-factor' ), array( 'status' => 500 ) );
}
ob_start();
$this->user_two_factor_options( $user );
$html = ob_get_clean();
return array(
'success' => true,
'html' => $html,
);
}
/**
* Generates a URL that can be used to create a QR code.
*
* @since 0.8.0
*
* @param WP_User $user The user to generate a URL for.
* @param string $secret_key The secret key.
*
* @return string
*/
public static function generate_qr_code_url( $user, $secret_key ) {
$issuer = get_bloginfo( 'name', 'display' );
/**
* Filters the Issuer for the TOTP.
*
* Must follow the TOTP format for a "issuer". Do not URL Encode.
*
* @since 0.8.0
*
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format#issuer
*
* @param string $issuer The issuer for TOTP.
*/
$issuer = apply_filters( 'two_factor_totp_issuer', $issuer );
/**
* Filters the Label for the TOTP.
*
* Must follow the TOTP format for a "label". Do not URL Encode.
*
* @since 0.4.7
*
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label
*
* @param string $totp_title The label for the TOTP.
* @param WP_User $user The User object.
* @param string $issuer The issuer of the TOTP. This should be the prefix of the result.
*/
$totp_title = apply_filters( 'two_factor_totp_title', $issuer . ':' . $user->user_login, $user, $issuer );
$totp_url = add_query_arg(
array(
'secret' => rawurlencode( $secret_key ),
'issuer' => rawurlencode( $issuer ),
),
'otpauth://totp/' . rawurlencode( $totp_title )
);
/**
* Filters the TOTP generated URL.
*
* Must follow the TOTP format. Do not URL Encode.
*
* @since 0.8.0
*
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
*
* @param string $totp_url The TOTP URL.
* @param WP_User $user The user object.
*/
$totp_url = apply_filters( 'two_factor_totp_url', $totp_url, $user );
$totp_url = esc_url_raw( $totp_url, array( 'otpauth' ) );
return $totp_url;
}
/**
* Display TOTP options on the user settings page.
*
* @since 0.2.0
*
* @param WP_User $user The current user being edited.
* @return void
*
* @codeCoverageIgnore
*/
public function user_two_factor_options( $user ) {
if ( ! isset( $user->ID ) ) {
return;
}
$key = $this->get_user_totp_key( $user->ID );
wp_localize_script(
'two-factor-totp-admin',
'twoFactorTotpAdmin',
array(
'restPath' => Two_Factor_Core::REST_NAMESPACE . '/totp',
'userId' => $user->ID,
'qrCodeAriaLabel' => __( 'Authenticator App QR Code', 'two-factor' ),
)
);
wp_enqueue_script( 'two-factor-totp-admin' );
?>
<div id="two-factor-totp-options">
<?php
if ( empty( $key ) ) :
$key = $this->generate_key();
$totp_url = $this->generate_qr_code_url( $user, $key );
?>
<p>
<?php esc_html_e( 'Please follow these steps in order to complete setup:', 'two-factor' ); ?>
</p>
<ol class="totp-steps">
<li>
<?php esc_html_e( 'Install an authenticator app on your desktop/laptop and/or phone. Popular examples are Microsoft Authenticator, Google Authenticator and Authy.', 'two-factor' ); ?>
</li>
<li>
<?php esc_html_e( 'Scan this QR code using the app you installed:', 'two-factor' ); ?>
<p id="two-factor-qr-code">
<a href="<?php echo esc_url( $totp_url, array( 'otpauth' ) ); ?>">
<?php esc_html_e( 'Loading…', 'two-factor' ); ?>
<img src="<?php echo esc_url( admin_url( 'images/spinner.gif' ) ); ?>" alt="" />
</a>
</p>
<p>
<?php
esc_html_e(
'If scanning isn\'t possible or doesn\'t work, click on the QR code or use the secret key shown below to add the account to your chosen app:',
'two-factor'
);
?>
</p>
<p>
<code><?php echo esc_html( $key ); ?></code>
</p>
</li>
<li>
<p><?php esc_html_e( 'Enter the code generated by the Authenticator app to complete the setup:', 'two-factor' ); ?></p>
<p>
<input type="hidden" id="two-factor-totp-key" name="two-factor-totp-key" value="<?php echo esc_attr( $key ); ?>" />
<label for="two-factor-totp-authcode">
<?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?>
<?php
/* translators: Example auth code. */
$placeholder = sprintf( __( 'eg. %s', 'two-factor' ), '123456' );
?>
<input type="text" inputmode="numeric" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $placeholder ); ?>" autocomplete="off" />
</label>
<input type="submit" class="button totp-submit" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Verify', 'two-factor' ); ?>" />
</p>
<p class="description">
<?php
printf(
/* translators: 1: server date and time */
esc_html__( 'If the code is rejected, check that your web server time is accurate: %1$s. Your device and server times must match.', 'two-factor' ),
sprintf(
'<time class="two-factor-server-datetime-epoch" datetime="%1$s">%2$s (%3$s)</time>',
esc_attr( wp_date( 'c' ) ),
esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ),
esc_html( wp_timezone_string() )
)
);
?>
</p>
</li>
</ol>
<?php
wp_localize_script(
'two-factor-totp-qrcode',
'twoFactorTotpQrcode',
array(
'totpUrl' => $totp_url,
'qrCodeLabel' => __( 'Authenticator App QR Code', 'two-factor' ),
)
);
wp_enqueue_script( 'two-factor-totp-qrcode' );
?>
<?php else : ?>
<p class="success">
<?php esc_html_e( 'An authenticator app is currently configured. You will need to re-scan the QR code on all devices if reset.', 'two-factor' ); ?>
</p>
<p>
<button type="button" class="button button-secondary reset-totp-key hide-if-no-js">
<?php esc_html_e( 'Reset authenticator app', 'two-factor' ); ?>
</button>
</p>
<?php endif; ?>
</div>
<?php
}
/**
* Get the TOTP secret key for a user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
*
* @return string
*/
public function get_user_totp_key( $user_id ) {
return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
}
/**
* Set the TOTP secret key for a user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
* @param string $key TOTP secret key.
*
* @return boolean If the key was stored successfully.
*/
public function set_user_totp_key( $user_id, $key ) {
return update_user_meta( $user_id, self::SECRET_META_KEY, $key );
}
/**
* Delete the TOTP secret key for a user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
*
* @return boolean If the key was deleted successfully.
*/
public function delete_user_totp_key( $user_id ) {
delete_user_meta( $user_id, self::LAST_SUCCESSFUL_LOGIN_META_KEY );
return delete_user_meta( $user_id, self::SECRET_META_KEY );
}
/**
* Check if the TOTP secret key has a proper format.
*
* @since 0.2.0
*
* @param string $key TOTP secret key.
*
* @return boolean
*/
public function is_valid_key( $key ) {
$check = sprintf( '/^[%s]+$/', self::$base_32_chars );
if ( 1 === preg_match( $check, $key ) ) {
return true;
}
return false;
}
/**
* Validates authentication.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @return bool Whether the user gave a valid code
*/
public function validate_authentication( $user ) {
$code = $this->sanitize_code_from_request( 'authcode', self::DEFAULT_DIGIT_COUNT );
if ( ! $code ) {
return false;
}
return $this->validate_code_for_user( $user, $code );
}
/**
* Validates an authentication code for a given user, preventing re-use and older TOTP keys.
*
* @since 0.8.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @param int $code The TOTP token to validate.
*
* @return bool Whether the code is valid for the user and a newer code has not been used.
*/
public function validate_code_for_user( $user, $code ) {
$valid_timestamp = $this->get_authcode_valid_ticktime(
$this->get_user_totp_key( $user->ID ),
$code
);
if ( ! $valid_timestamp ) {
return false;
}
$last_totp_login = (int) get_user_meta( $user->ID, self::LAST_SUCCESSFUL_LOGIN_META_KEY, true );
// The TOTP authentication is not valid, if we've seen the same or newer code.
if ( $last_totp_login && $last_totp_login >= $valid_timestamp ) {
return false;
}
update_user_meta( $user->ID, self::LAST_SUCCESSFUL_LOGIN_META_KEY, $valid_timestamp );
return true;
}
/**
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift.
*
* @since 0.15.0
*
* @param string $key The share secret key to use.
* @param string $authcode The code to test.
* @param string $hash The hash used to calculate the code.
* @param int $time_step The size of the time step.
*
* @return bool Whether the code is valid within the time frame.
*/
public static function is_valid_authcode( $key, $authcode, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
return (bool) self::get_authcode_valid_ticktime( $key, $authcode, $hash, $time_step );
}
/**
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift.
*
* @since 0.15.0
*
* @param string $key The share secret key to use.
* @param string $authcode The code to test.
* @param string $hash The hash used to calculate the code.
* @param int $time_step The size of the time step.
*
* @return false|int Returns the timestamp of the authcode on success, False otherwise.
*/
public static function get_authcode_valid_ticktime( $key, $authcode, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
/**
* Filter the maximum ticks to allow when checking valid codes.
*
* Ticks are the allowed offset from the correct time in 30 second increments,
* so the default of 4 allows codes that are two minutes to either side of server time.
*
* @since 0.2.0
* @deprecated 0.7.0 Use {@see 'two_factor_totp_time_step_allowance'} instead.
*
* @param int $max_ticks Max ticks of time correction to allow. Default 4.
*/
$max_ticks = apply_filters_deprecated( 'two-factor-totp-time-step-allowance', array( self::DEFAULT_TIME_STEP_ALLOWANCE ), '0.7.0', 'two_factor_totp_time_step_allowance' );
/**
* Filters the maximum ticks to allow when checking valid codes.
*
* Ticks are the allowed offset from the correct time in 30 second increments,
* so the default of 4 allows codes that are two minutes to either side of server time.
*
* @since 0.7.0
*
* @param int $max_ticks Max ticks of time correction to allow. Default 4.
*/
$max_ticks = apply_filters( 'two_factor_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
// Array of all ticks to allow, sorted using absolute value to test closest match first.
$ticks = range( - $max_ticks, $max_ticks );
usort( $ticks, array( __CLASS__, 'abssort' ) );
$time = (int) floor( self::time() / $time_step );
$digits = strlen( $authcode );
foreach ( $ticks as $offset ) {
$log_time = (int) ( $time + $offset );
if ( hash_equals( self::calc_totp( $key, $log_time, $digits, $hash, $time_step ), $authcode ) ) {
// Return the tick timestamp.
return (int) ( $log_time * self::DEFAULT_TIME_STEP_SEC );
}
}
return false;
}
/**
* Generates key
*
* @since 0.2.0
*
* @param int $bitsize Nume of bits to use for key.
*
* @return string $bitsize long string composed of available base32 chars.
*/
public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
$bytes = ceil( $bitsize / 8 );
$secret = wp_generate_password( $bytes, true, true );
return self::base32_encode( $secret );
}
/**
* Pack stuff. We're currently only using this to pack integers, however the generic `pack` method can handle mixed.
*
* @since 0.2.0
*
* @param int $value The value to be packed.
*
* @return string Binary packed string.
*/
public static function pack64( int $value ): string {
// Native 64-bit support (modern PHP on 64-bit builds).
if ( 8 === PHP_INT_SIZE ) {
return pack( 'J', $value );
}
// 32-bit PHP fallback
$higher = ( $value >> 32 ) & 0xFFFFFFFF;
$lower = $value & 0xFFFFFFFF;
return pack( 'NN', $higher, $lower );
}
/**
* Pad a short secret with bytes from the same until it's the correct length
* for hashing.
*
* @since 0.15.0
*
* @param string $secret Secret key to pad.
* @param int $length Byte length of the desired padded secret.
*
* @throws InvalidArgumentException If the secret or length are invalid.
*
* @return string
*/
protected static function pad_secret( $secret, $length ) {
if ( empty( $secret ) ) {
throw new InvalidArgumentException( 'Secret must be non-empty!' );
}
$length = intval( $length );
if ( $length <= 0 ) {
throw new InvalidArgumentException( 'Padding length must be non-zero' );
}
return str_pad( $secret, $length, $secret, STR_PAD_RIGHT );
}
/**
* Calculate a valid code given the shared secret key
*
* @since 0.2.0
*
* @param string $key The shared secret key to use for calculating code.
* @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size.
* @param int $digits The number of digits in the returned code.
* @param string $hash The hash used to calculate the code.
* @param int $time_step The size of the time step.
*
* @throws InvalidArgumentException If the hash type is invalid.
*
* @return string The totp code
*/
public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
$secret = self::base32_decode( $key );
switch ( $hash ) {
case 'sha1':
$secret = self::pad_secret( $secret, 20 );
break;
case 'sha256':
$secret = self::pad_secret( $secret, 32 );
break;
case 'sha512':
$secret = self::pad_secret( $secret, 64 );
break;
default:
throw new InvalidArgumentException( 'Invalid hash type specified!' );
}
if ( false === $step_count ) {
$step_count = floor( self::time() / $time_step );
}
$timestamp = self::pack64( $step_count );
$hash = hash_hmac( $hash, $timestamp, $secret, true );
$offset = ord( $hash[ strlen( $hash ) - 1 ] ) & 0xf;
$code = (
( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
( ord( $hash[ $offset + 3 ] ) & 0xff )
) % pow( 10, $digits );
return str_pad( $code, $digits, '0', STR_PAD_LEFT );
}
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @return boolean
*/
public function is_available_for_user( $user ) {
// Only available if the secret key has been saved for the user.
$key = $this->get_user_totp_key( $user->ID );
return ! empty( $key );
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @codeCoverageIgnore
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_before_authentication_prompt', $this );
?>
<p class="two-factor-prompt">
<?php esc_html_e( 'Enter the code generated by your authenticator app.', 'two-factor' ); ?>
</p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_prompt', $this );
?>
<p>
<label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" name="authcode" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="123 456" autocomplete="one-time-code" data-digits="<?php echo esc_attr( self::DEFAULT_DIGIT_COUNT ); ?>" />
</p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_input', $this );
?>
<?php
wp_enqueue_script( 'two-factor-login' );
submit_button( __( 'Verify', 'two-factor' ) );
}
/**
* Returns a base32 encoded string.
*
* @since 0.2.0
*
* @param string $input String to be encoded using base32.
*
* @return string base32 encoded string without padding.
*/
public static function base32_encode( $input ) {
if ( empty( $input ) ) {
return '';
}
$binary_string = '';
foreach ( str_split( $input ) as $character ) {
$binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
}
$five_bit_sections = str_split( $binary_string, 5 );
$base32_string = '';
foreach ( $five_bit_sections as $five_bit_section ) {
$base32_string .= self::$base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
}
return $base32_string;
}
/**
* Decode a base32 string and return a binary representation
*
* @since 0.2.0
*
* @param string $base32_string The base 32 string to decode.
*
* @throws Exception If string contains non-base32 characters.
*
* @return string Binary representation of decoded string
*/
public static function base32_decode( $base32_string ) {
$base32_string = strtoupper( $base32_string );
if ( ! preg_match( '/^[' . self::$base_32_chars . ']+$/', $base32_string, $match ) ) {
throw new Exception( 'Invalid characters in the base32 string.' );
}
$l = strlen( $base32_string );
$n = 0;
$j = 0;
$binary = '';
for ( $i = 0; $i < $l; $i++ ) {
$n = $n << 5; // Move buffer left by 5 to make room.
$n = $n + strpos( self::$base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
$j += 5; // Keep track of number of bits in buffer.
if ( $j >= 8 ) {
$j -= 8;
$binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
}
}
return $binary;
}
/**
* Used with usort to sort an array by distance from 0
*
* @since 0.2.0
*
* @param int $a First array element.
* @param int $b Second array element.
*
* @return int -1, 0, or 1 as needed by usort
*/
private static function abssort( $a, $b ) {
$a = abs( $a );
$b = abs( $b );
if ( $a === $b ) {
return 0;
}
return ( $a < $b ) ? -1 : 1;
}
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {
return array(
self::SECRET_META_KEY,
self::LAST_SUCCESSFUL_LOGIN_META_KEY,
);
}
}
@@ -0,0 +1,49 @@
/* global twoFactorBackupCodes, wp, navigator, document, jQuery */
( function( $ ) {
$( '.button-two-factor-backup-codes-copy' ).click( function() {
var csvCodes = $( '.two-factor-backup-codes-wrapper' ).data( 'codesCsv' ),
$temp;
if ( ! csvCodes ) {
return;
}
if ( navigator.clipboard && navigator.clipboard.writeText ) {
navigator.clipboard.writeText( csvCodes );
return;
}
$temp = $( '<textarea>' ).val( csvCodes ).css( { position: 'absolute', left: '-9999px' } );
$( 'body' ).append( $temp );
$temp[0].select();
document.execCommand( 'copy' );
$temp.remove();
} );
$( '.button-two-factor-backup-codes-generate' ).click( function() {
wp.apiRequest( {
method: 'POST',
path: twoFactorBackupCodes.restPath,
data: {
user_id: parseInt( twoFactorBackupCodes.userId, 10 )
}
} ).then( function( response ) {
var $codesList = $( '.two-factor-backup-codes-unused-codes' ),
i;
$( '.two-factor-backup-codes-wrapper' ).show();
$codesList.html( '' );
$codesList.css( { 'column-count': 2, 'column-gap': '80px', 'max-width': '420px' } );
$( '.two-factor-backup-codes-wrapper' ).data( 'codesCsv', response.codes.join( ',' ) );
// Append the codes.
for ( i = 0; i < response.codes.length; i++ ) {
$codesList.append( '<li class="two-factor-backup-codes-token">' + response.codes[ i ] + '</li>' );
}
// Update counter.
$( '.two-factor-backup-codes-count' ).html( response.i18n.count );
$( '#two-factor-backup-codes-download-link' ).attr( 'href', response.download_link );
} );
} );
}( jQuery ) );
@@ -0,0 +1,35 @@
/* global twoFactorTotpQrcode, qrcode, document, window */
( function() {
var qrGenerator = function() {
/*
* 0 = Automatically select the version, to avoid going over the limit of URL
* length.
* L = Least amount of error correction, because it's not needed when scanning
* on a monitor, and it lowers the image size.
*/
var qr = qrcode( 0, 'L' ),
svg,
title;
qr.addData( twoFactorTotpQrcode.totpUrl );
qr.make();
document.querySelector( '#two-factor-qr-code a' ).innerHTML = qr.createSvgTag( 5 );
// For accessibility, markup the SVG with a title and role.
svg = document.querySelector( '#two-factor-qr-code a svg' );
title = document.createElement( 'title' );
svg.setAttribute( 'role', 'img' );
svg.setAttribute( 'aria-label', twoFactorTotpQrcode.qrCodeLabel );
title.innerText = twoFactorTotpQrcode.qrCodeLabel;
svg.appendChild( title );
};
// Run now if the document is loaded, otherwise on DOMContentLoaded.
if ( document.readyState === 'complete' ) {
qrGenerator();
} else {
window.addEventListener( 'DOMContentLoaded', qrGenerator );
}
}() );
@@ -0,0 +1,95 @@
/* global twoFactorTotpAdmin, qrcode, wp, document, jQuery */
( function( $ ) {
var generateQrCode = function( totpUrl ) {
var $qrLink = $( '#two-factor-qr-code a' ),
qr,
svg,
title;
if ( ! $qrLink.length || typeof qrcode === 'undefined' ) {
return;
}
qr = qrcode( 0, 'L' );
qr.addData( totpUrl );
qr.make();
$qrLink.html( qr.createSvgTag( 5 ) );
svg = $qrLink.find( 'svg' )[ 0 ];
if ( svg ) {
var ariaLabel = ( typeof twoFactorTotpAdmin !== 'undefined' && twoFactorTotpAdmin && twoFactorTotpAdmin.qrCodeAriaLabel ) ? twoFactorTotpAdmin.qrCodeAriaLabel : 'Authenticator App QR Code';
title = document.createElement( 'title' );
svg.setAttribute( 'role', 'img' );
svg.setAttribute( 'aria-label', ariaLabel );
title.innerText = ariaLabel;
svg.appendChild( title );
}
};
var checkbox = document.getElementById( 'enabled-Two_Factor_Totp' );
// Focus the auth code input when the checkbox is clicked.
if ( checkbox ) {
checkbox.addEventListener( 'click', function( e ) {
if ( e.target.checked ) {
document.getElementById( 'two-factor-totp-authcode' ).focus();
}
} );
}
$( '.totp-submit' ).click( function( e ) {
var key = $( '#two-factor-totp-key' ).val(),
code = $( '#two-factor-totp-authcode' ).val();
e.preventDefault();
wp.apiRequest( {
method: 'POST',
path: twoFactorTotpAdmin.restPath,
data: {
user_id: parseInt( twoFactorTotpAdmin.userId, 10 ),
key: key,
code: code,
enable_provider: true
}
} ).fail( function( response, status ) {
var errorMessage = ( response && response.responseJSON && response.responseJSON.message ) || ( response && response.statusText ) || status || '',
$error = $( '#totp-setup-error' );
if ( ! $error.length ) {
$error = $( '<div class="error" id="totp-setup-error"><p></p></div>' ).insertAfter( $( '.totp-submit' ) );
}
$error.find( 'p' ).text( errorMessage );
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ).trigger( 'change' );
$( '#two-factor-totp-authcode' ).val( '' );
} ).then( function( response ) {
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', true ).trigger( 'change' );
$( '#two-factor-totp-options' ).html( response.html );
} );
} );
$( '.button.reset-totp-key' ).click( function( e ) {
e.preventDefault();
wp.apiRequest( {
method: 'DELETE',
path: twoFactorTotpAdmin.restPath,
data: {
user_id: parseInt( twoFactorTotpAdmin.userId, 10 )
}
} ).then( function( response ) {
var totpUrl;
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false );
$( '#two-factor-totp-options' ).html( response.html );
totpUrl = $( '#two-factor-qr-code a' ).attr( 'href' );
if ( totpUrl ) {
generateQrCode( totpUrl );
}
} );
} );
}( jQuery ) );
@@ -0,0 +1,38 @@
/* global document */
( function() {
// Enforce numeric-only input for numeric inputmode elements.
var form = document.querySelector( '#loginform' ),
inputEl = document.querySelector( 'input.authcode[inputmode="numeric"]' ),
expectedLength = ( inputEl && inputEl.dataset ) ? inputEl.dataset.digits : 0,
spaceInserted = false;
if ( inputEl ) {
inputEl.addEventListener(
'input',
function() {
var value = this.value.replace( /[^0-9 ]/g, '' ).replace( /^\s+/, '' ),
submitControl;
if ( ! spaceInserted && expectedLength && value.length === Math.floor( expectedLength / 2 ) ) {
value += ' ';
spaceInserted = true;
} else if ( spaceInserted && ! this.value ) {
spaceInserted = false;
}
this.value = value;
// Auto-submit if it's the expected length.
if ( expectedLength && value.replace( / /g, '' ).length === parseInt( expectedLength, 10 ) ) {
if ( form && typeof form.requestSubmit === 'function' ) {
form.requestSubmit();
submitControl = form.querySelector( '[type="submit"]' );
if ( submitControl ) {
submitControl.disabled = true;
}
}
}
}
);
}
}() );
@@ -0,0 +1,11 @@
/* global document, setTimeout */
( function() {
setTimeout( function() {
var d;
try {
d = document.getElementById( 'authcode' );
d.value = '';
d.focus();
} catch ( e ) {}
}, 200 );
}() );
@@ -0,0 +1,259 @@
=== Two Factor ===
Contributors: georgestephanis, kasparsd, masteradhoc, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, alihusnainarshad, passoniate
Tags: 2fa, mfa, totp, authentication, security
Tested up to: 6.9
Stable tag: 0.16.0
License: GPL-2.0-or-later
License URI: https://spdx.org/licenses/GPL-2.0-or-later.html
Enable Two-Factor Authentication (2FA) using time-based one-time passwords (TOTP), email, and backup verification codes.
== Description ==
The Two-Factor plugin adds an extra layer of security to your WordPress login by requiring users to provide a second form of authentication in addition to their password. This helps protect against unauthorized access even if passwords are compromised.
## Setup Instructions
**Important**: Each user must individually configure their two-factor authentication settings.
### For Individual Users
1. **Navigate to your profile**: Go to "Users" → "Your Profile" in the WordPress admin
2. **Find Two-Factor Options**: Scroll down to the "Two-Factor Options" section
3. **Choose your methods**: Enable one or more authentication providers (noting a site admin may have hidden one or more so what is available could vary):
- **Authenticator App (TOTP)** - Use apps like Google Authenticator, Authy, or 1Password
- **Email Codes** - Receive one-time codes via email
- **Backup Codes** - Generate one-time backup codes for emergencies
- **Dummy Method** - For testing purposes only (requires WP_DEBUG)
4. **Configure each method**: Follow the setup instructions for each enabled provider
5. **Set primary method**: Choose which method to use as your default authentication
6. **Save changes**: Click "Update Profile" to save your settings
### For Site Administrators
- **Plugin settings**: The plugin provides a settings page under "Settings → Two-Factor" to configure which providers should be disabled site-wide.
- **User management**: Administrators can configure 2FA for other users by editing their profiles
- **Security recommendations**: Encourage users to enable backup methods to prevent account lockouts
## Available Authentication Methods
### Authenticator App (TOTP) - Recommended
- **Security**: High - Time-based one-time passwords
- **Setup**: Scan QR code with authenticator app
- **Compatibility**: Works with Google Authenticator, Authy, 1Password, and other TOTP apps
- **Best for**: Most users, provides excellent security with good usability
### Backup Codes - Recommended
- **Security**: Medium - One-time use codes
- **Setup**: Generate 10 backup codes for emergency access
- **Compatibility**: Works everywhere, no special hardware needed
- **Best for**: Emergency access when other methods are unavailable
### Email Codes
- **Security**: Medium - One-time codes sent via email
- **Setup**: Automatic - uses your WordPress email address
- **Compatibility**: Works with any email-capable device
- **Best for**: Users who prefer email-based authentication
### FIDO U2F Security Keys
- Deprecated and removed due to loss of browser support.
### Dummy Method
- **Security**: None - Always succeeds
- **Setup**: Only available when WP_DEBUG is enabled
- **Purpose**: Testing and development only
- **Best for**: Developers testing the plugin
## Important Notes
### HTTPS Requirement
- All methods work on both HTTP and HTTPS sites
### Browser Compatibility
- TOTP and email methods work on all devices and browsers
### Account Recovery
- Always enable backup codes to prevent being locked out of your account
- If you lose access to all authentication methods, contact your site administrator
### Security Best Practices
- Use multiple authentication methods when possible
- Keep backup codes in a secure location
- Regularly review and update your authentication settings
For more information about two-factor authentication in WordPress, see the [WordPress Advanced Administration Security Guide](https://developer.wordpress.org/advanced-administration/security/mfa/).
For more history, see [this post](https://georgestephanis.wordpress.com/2013/08/14/two-cents-on-two-factor/).
= Actions & Filters =
Here is a list of action and filter hooks provided by the plugin:
- `two_factor_providers` filter overrides the available two-factor providers such as email and time-based one-time passwords. Array values are PHP classnames of the two-factor providers.
- `two_factor_providers_for_user` filter overrides the available two-factor providers for a specific user. Array values are instances of provider classes and the user object `WP_User` is available as the second argument.
- `two_factor_enabled_providers_for_user` filter overrides the list of two-factor providers enabled for a user. First argument is an array of enabled provider classnames as values, the second argument is the user ID.
- `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow.
- `two_factor_user_api_login_enable` filter restricts authentication for REST API and XML-RPC to application passwords only. Provides the user ID as the second argument.
- `two_factor_email_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated.
- `two_factor_email_token_length` filter overrides the default 8 character count for email tokens.
- `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Provides the `WP_User` of the associated user as the second argument.
- `two_factor_rest_api_can_edit_user` filter overrides whether a users Two-Factor settings can be edited via the REST API. First argument is the current `$can_edit` boolean, the second argument is the user ID.
- `two_factor_before_authentication_prompt` action which receives the provider object and fires prior to the prompt shown on the authentication input form.
- `two_factor_after_authentication_prompt` action which receives the provider object and fires after the prompt shown on the authentication input form.
- `two_factor_after_authentication_input` action which receives the provider object and fires after the input shown on the authentication input form (if form contains no input, action fires immediately after `two_factor_after_authentication_prompt`).
- `two_factor_login_backup_links` filters the backup links displayed on the two-factor login form.
== Redirect After the Two-Factor Challenge ==
To redirect users to a specific URL after completing the two-factor challenge, use WordPress Core built-in login_redirect filter. The filter works the same way as in a standard WordPress login flow:
add_filter( 'login_redirect', function( $redirect_to, $requested_redirect_to, $user ) {
return home_url( '/dashboard/' );
}, 10, 3 );
== Frequently Asked Questions ==
= What PHP and WordPress versions does the Two-Factor plugin support? =
This plugin supports the last two major versions of WordPress and <a href="https://make.wordpress.org/core/handbook/references/php-compatibility-and-wordpress-versions/">the minimum PHP version</a> supported by those WordPress versions.
= How can I send feedback or get help with a bug? =
The best place to report bugs, feature suggestions, or any other (non-security) feedback is at <a href="https://github.com/WordPress/two-factor/issues">the Two Factor GitHub issues page</a>. Before submitting a new issue, please search the existing issues to check if someone else has reported the same feedback.
= Where can I report security bugs? =
The plugin contributors and WordPress community take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program.
= What if I lose access to all my authentication methods? =
If you have backup codes enabled, you can use one of those to regain access. If you don't have backup codes or have used them all, you'll need to contact your site administrator to reset your account. This is why it's important to always enable backup codes and keep them in a secure location.
= Can I use this plugin with WebAuthn? =
The plugin previously supported FIDO U2F, which was a predecessor to WebAuthn. There is an open issue to add WebAuthn support here: https://github.com/WordPress/two-factor/pull/427
= Is there a recommended way to use passkeys or hardware security keys with Two-Factor? =
Yes. For passkeys and hardware security keys, you can install the Two-Factor Provider: WebAuthn plugin: https://wordpress.org/plugins/two-factor-provider-webauthn/
. It integrates directly with Two-Factor and adds WebAuthn-based authentication as an additional two-factor option for users.
== Screenshots ==
1. Two-factor options under User Profile - Shows the main configuration area where users can enable different authentication methods.
2. Email Code Authentication during WordPress Login - Shows the email verification screen that appears during login.
3. Authenticator App (TOTP) setup with QR code - Demonstrates the QR code generation and manual key entry for TOTP setup.
4. Backup codes generation and management - Shows the backup codes interface for generating and managing emergency access codes.
== Changelog ==
= 0.16.0 - 2026-03-27 =
* **Breaking Changes:** Remove legacy FIDO U2F provider support by [#439](https://github.com/WordPress/two-factor/pull/439).
* **New Features:** Add a dedicated settings page for plugin configuration in wp-admin by [#764](https://github.com/WordPress/two-factor/pull/764).
* **New Features:** Add a support links filter so consumers can customize contextual recovery/help links by [#615](https://github.com/WordPress/two-factor/pull/615).
* **New Features:** Refresh backup codes UI styling and behavior by [#804](https://github.com/WordPress/two-factor/pull/804).
* **Bug Fixes:** Delete stored TOTP secrets when the TOTP provider is disabled by [#802](https://github.com/WordPress/two-factor/pull/802).
* **Bug Fixes:** Harden provider handling so login/settings checks do not fail open when expected providers disappear by [#586](https://github.com/WordPress/two-factor/pull/586).
* **Bug Fixes:** Ensure only configured providers are saved and enabled in user settings by [#798](https://github.com/WordPress/two-factor/pull/798).
* **Bug Fixes:** Improve settings-page accessibility and fix profile settings link behavior by [#828](https://github.com/WordPress/two-factor/pull/828) and [#830](https://github.com/WordPress/two-factor/pull/830).
* **Bug Fixes:** Resolve PHPCS violations in provider files by [#851](https://github.com/WordPress/two-factor/pull/851).
* **Development Updates:** Move login styles and provider scripts from inline output to enqueued/external assets by [#807](https://github.com/WordPress/two-factor/pull/807) and [#814](https://github.com/WordPress/two-factor/pull/814).
* **Development Updates:** Improve inline docs and static-analysis compatibility (WPCS/phpstan) by [#810](https://github.com/WordPress/two-factor/pull/810), [#815](https://github.com/WordPress/two-factor/pull/815), and [#817](https://github.com/WordPress/two-factor/pull/817).
* **Development Updates:** Improve unit test reliability and integrate CI code coverage reporting by [#825](https://github.com/WordPress/two-factor/pull/825), [#841](https://github.com/WordPress/two-factor/pull/841), and [#842](https://github.com/WordPress/two-factor/pull/842).
* **Development Updates:** Update readme docs and modernize CI workflow infrastructure by [#835](https://github.com/WordPress/two-factor/pull/835), [#837](https://github.com/WordPress/two-factor/pull/837), [#843](https://github.com/WordPress/two-factor/pull/843), and [#849](https://github.com/WordPress/two-factor/pull/849).
* **Dependency Updates:** Bump `qs` from 6.14.1 to 6.14.2 by [#794](https://github.com/WordPress/two-factor/pull/794).
* **Dependency Updates:** Bump `basic-ftp` from 5.0.5 to 5.2.0 by [#816](https://github.com/WordPress/two-factor/pull/816).
* **Dependency Updates:** Apply automatic lint/format updates and associated Composer package refreshes by [#799](https://github.com/WordPress/two-factor/pull/799).
= 0.15.0 - 2026-02-13 =
* **Breaking Changes:** Trigger two-factor flow only when expected by @kasparsd in [#660](https://github.com/WordPress/two-factor/pull/660) and [#793](https://github.com/WordPress/two-factor/pull/793).
* **New Features:** Include user IP address and contextual warning in two-factor code emails by @todeveni in [#728](https://github.com/WordPress/two-factor/pull/728)
* **New Features:** Optimize email text for TOTP by @masteradhoc in [#789](https://github.com/WordPress/two-factor/pull/789)
* **New Features:** Add "Settings" action link to plugin list for quick access to profile by @hardikRathi in [#740](https://github.com/WordPress/two-factor/pull/740)
* **New Features:** Additional form hooks by @eric-michel in [#742](https://github.com/WordPress/two-factor/pull/742)
* **New Features:** Full RFC6238 Compatibility by @ericmann in [#656](https://github.com/WordPress/two-factor/pull/656)
* **New Features:** Consistent user experience for TOTP setup by @kasparsd in [#792](https://github.com/WordPress/two-factor/pull/792)
* **Documentation:** `@since` docs by @masteradhoc in [#781](https://github.com/WordPress/two-factor/pull/781)
* **Documentation:** Update user and admin docs, prepare for more screenshots by @jeffpaul in [#701](https://github.com/WordPress/two-factor/pull/701)
* **Documentation:** Add changelog & credits, update release notes by @jeffpaul in [#696](https://github.com/WordPress/two-factor/pull/696)
* **Documentation:** Clear readme.txt by @masteradhoc in [#785](https://github.com/WordPress/two-factor/pull/785)
* **Documentation:** Add date and time information above TOTP setup instructions by @masteradhoc in [#772](https://github.com/WordPress/two-factor/pull/772)
* **Documentation:** Clarify TOTP setup instructions by @masteradhoc in [#763](https://github.com/WordPress/two-factor/pull/763)
* **Documentation:** Update RELEASING.md by @jeffpaul in [#787](https://github.com/WordPress/two-factor/pull/787)
* **Development Updates:** Pause deploys to SVN trunk for merges to `master` by @kasparsd in [#738](https://github.com/WordPress/two-factor/pull/738)
* **Development Updates:** Fix CI checks for PHP compatability by @kasparsd in [#739](https://github.com/WordPress/two-factor/pull/739)
* **Development Updates:** Fix Playground refs by @kasparsd in [#744](https://github.com/WordPress/two-factor/pull/744)
* **Development Updates:** Persist existing translations when introducing new helper text in emails by @kasparsd in [#745](https://github.com/WordPress/two-factor/pull/745)
* **Development Updates:** Fix `missing_direct_file_access_protection` by @masteradhoc in [#760](https://github.com/WordPress/two-factor/pull/760)
* **Development Updates:** Fix `mismatched_plugin_name` by @masteradhoc in [#754](https://github.com/WordPress/two-factor/pull/754)
* **Development Updates:** Introduce Props Bot workflow by @jeffpaul in [#749](https://github.com/WordPress/two-factor/pull/749)
* **Development Updates:** Plugin Check: Fix Missing $domain parameter by @masteradhoc in [#753](https://github.com/WordPress/two-factor/pull/753)
* **Development Updates:** Tests: Update to supported WP version 6.8 by @masteradhoc in [#770](https://github.com/WordPress/two-factor/pull/770)
* **Development Updates:** Fix PHP 8.5 deprecated message by @masteradhoc in [#762](https://github.com/WordPress/two-factor/pull/762)
* **Development Updates:** Exclude 7.2 and 7.3 checks against trunk by @masteradhoc in [#769](https://github.com/WordPress/two-factor/pull/769)
* **Development Updates:** Fix Plugin Check errors: `MissingTranslatorsComment` & `MissingSingularPlaceholder` by @masteradhoc in [#758](https://github.com/WordPress/two-factor/pull/758)
* **Development Updates:** Add PHP 8.5 tests for latest and trunk version of WP by @masteradhoc in [#771](https://github.com/WordPress/two-factor/pull/771)
* **Development Updates:** Add `phpcs:ignore` for falsepositives by @masteradhoc in [#777](https://github.com/WordPress/two-factor/pull/777)
* **Development Updates:** Fix(totp): `otpauth` link in QR code URL by @sjinks in [#784](https://github.com/WordPress/two-factor/pull/784)
* **Development Updates:** Update deploy.yml by @masteradhoc in [#773](https://github.com/WordPress/two-factor/pull/773)
* **Development Updates:** Update required WordPress Version by @masteradhoc in [#765](https://github.com/WordPress/two-factor/pull/765)
* **Development Updates:** Fix: ensure execution stops after redirects by @sjinks in [#786](https://github.com/WordPress/two-factor/pull/786)
* **Development Updates:** Fix `WordPress.Security.EscapeOutput.OutputNotEscaped` errors by @masteradhoc in [#776](https://github.com/WordPress/two-factor/pull/776)
* **Dependency Updates:** Bump qs and express by @dependabot[bot] in [#746](https://github.com/WordPress/two-factor/pull/746)
* **Dependency Updates:** Bump lodash from 4.17.21 to 4.17.23 by @dependabot[bot] in [#750](https://github.com/WordPress/two-factor/pull/750)
* **Dependency Updates:** Bump lodash-es from 4.17.21 to 4.17.23 by @dependabot[bot] in [#748](https://github.com/WordPress/two-factor/pull/748)
* **Dependency Updates:** Bump phpunit/phpunit from 8.5.44 to 8.5.52 by @dependabot[bot] in [#755](https://github.com/WordPress/two-factor/pull/755)
* **Dependency Updates:** Bump symfony/process from 5.4.47 to 5.4.51 by @dependabot[bot] in [#756](https://github.com/WordPress/two-factor/pull/756)
* **Dependency Updates:** Bump qs and body-parser by @dependabot[bot] in [#782](https://github.com/WordPress/two-factor/pull/782)
* **Dependency Updates:** Bump webpack from 5.101.3 to 5.105.0 by @dependabot[bot] in [#780](https://github.com/WordPress/two-factor/pull/780)
= 0.14.2 - 2025-12-11 =
* **New Features:** Add filter for rest_api_can_edit_user_and_update_two_factor_options by @gutobenn in [#689](https://github.com/WordPress/two-factor/pull/689)
* **Development Updates:** Remove Coveralls tooling and add inline coverage report by @kasparsd in [#717](https://github.com/WordPress/two-factor/pull/717)
* **Development Updates:** Update blueprint path to pull from main branch instead of a deleted f… by @georgestephanis in [#719](https://github.com/WordPress/two-factor/pull/719)
* **Development Updates:** Fix blueprint and wporg asset deploys by @kasparsd in [#734](https://github.com/WordPress/two-factor/pull/734)
* **Development Updates:** Upload release only on tag releases by @kasparsd in [#735](https://github.com/WordPress/two-factor/pull/735)
* **Development Updates:** Bump playwright and @playwright/test by @dependabot[bot] in [#721](https://github.com/WordPress/two-factor/pull/721)
* **Development Updates:** Bump tar-fs from 3.1.0 to 3.1.1 by @dependabot[bot] in [#720](https://github.com/WordPress/two-factor/pull/720)
* **Development Updates:** Bump node-forge from 1.3.1 to 1.3.2 by @dependabot[bot] in [#724](https://github.com/WordPress/two-factor/pull/724)
* **Development Updates:** Bump js-yaml by @dependabot[bot] in [#725](https://github.com/WordPress/two-factor/pull/725)
* **Development Updates:** Mark as tested with the latest WP core version by @kasparsd in [#730](https://github.com/WordPress/two-factor/pull/730)
= 0.14.1 - 2025-09-05 =
- Don't URI encode the TOTP url for display. by @dd32 in [#711](https://github.com/WordPress/two-factor/pull/711)
- Removed the duplicate Security.md by @slvignesh05 in [#712](https://github.com/WordPress/two-factor/pull/712)
- Fixed linting issues by @sudar in [#707](https://github.com/WordPress/two-factor/pull/707)
- Update development dependencies and fix failing QR unit test by @kasparsd in [#714](https://github.com/WordPress/two-factor/pull/714)
- Trigger checkbox js change event by @gedeminas in [#688](https://github.com/WordPress/two-factor/pull/688)
= 0.14.0 - 2025-07-03 =
* **Features:** Enable Application Passwords for REST API and XML-RPC authentication (by default) by @joostdekeijzer in [#697](https://github.com/WordPress/two-factor/pull/697) and [#698](https://github.com/WordPress/two-factor/pull/698). Previously this required two_factor_user_api_login_enable filter to be set to true which is now the default during application password auth. XML-RPC login is still disabled for regular user passwords.
* **Features:** Label recommended methods to simplify the configuration by @kasparsd in [#676](https://github.com/WordPress/two-factor/pull/676) and [#675](https://github.com/WordPress/two-factor/pull/675)
* **Documentation:** Add WP.org plugin demo by @kasparsd in [#667](https://github.com/WordPress/two-factor/pull/667)
* **Documentation:** Document supported versions of WP core and PHP by @jeffpaul in [#695](https://github.com/WordPress/two-factor/pull/695)
* **Documentation:** Document the release process by @jeffpaul in [#684](https://github.com/WordPress/two-factor/pull/684)
* **Tooling:** Remove duplicate WP.org screenshots and graphics from SVN trunk by @jeffpaul in [#683](https://github.com/WordPress/two-factor/pull/683)
= 0.13.0 - 2025-04-02 =
- Add two_factor_providers_for_user filter to limit two-factor providers available to each user by @kasparsd in [#669](https://github.com/WordPress/two-factor/pull/669)
- Update automated testing to cover PHP 8.4 and default to PHP 8.3 by @BrookeDot in [#665](https://github.com/WordPress/two-factor/pull/665)
[View the complete changelog details here](https://github.com/wordpress/two-factor/blob/master/CHANGELOG.md).
== Upgrade Notice ==
= 0.10.0 =
Bumps WordPress minimum supported version to 6.3 and PHP minimum to 7.2.
= 0.9.0 =
Users are now asked to re-authenticate with their two-factor before making changes to their two-factor settings. This associates each login session with the two-factor login meta data for improved handling of that session.
@@ -0,0 +1,97 @@
<?php
/**
* Admin settings UI for the Two-Factor plugin.
* Provides a site-wide settings screen for disabling individual Two-Factor providers.
*
* @since 0.16
*
* @package Two_Factor
*/
/**
* Settings screen renderer for Two-Factor.
*
* @since 0.16
*/
class Two_Factor_Settings {
/**
* Render the settings page.
* Also handles saving of settings when the form is submitted.
*
* @since 0.16
*
* @return void
*/
public static function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Handle save.
if ( isset( $_POST['two_factor_settings_submit'] ) ) {
check_admin_referer( 'two_factor_save_settings', 'two_factor_settings_nonce' );
$posted = isset( $_POST['two_factor_enabled_providers'] ) && is_array( $_POST['two_factor_enabled_providers'] ) ? wp_unslash( $_POST['two_factor_enabled_providers'] ) : array();
// Sanitize posted values immediately.
$posted = array_map( 'sanitize_text_field', (array) $posted );
// Remove empty values.
$enabled = array_values( array_filter( $posted, 'strlen' ) );
update_option( 'two_factor_enabled_providers', array_values( array_unique( $enabled ) ) );
echo '<div class="updated"><p>' . esc_html__( 'Settings saved.', 'two-factor' ) . '</p></div>';
}
// Build provider list for display using public core API.
$provider_instances = array();
if ( class_exists( 'Two_Factor_Core' ) && method_exists( 'Two_Factor_Core', 'get_providers' ) ) {
$provider_instances = Two_Factor_Core::get_providers();
if ( ! is_array( $provider_instances ) ) {
$provider_instances = array();
}
}
// Default to all providers enabled when the option has never been saved.
$all_provider_keys = array_keys( $provider_instances );
$saved_enabled = get_option( 'two_factor_enabled_providers', $all_provider_keys );
echo '<div class="wrap two-factor-settings">';
echo '<h1>' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '</h1>';
echo '<h2>' . esc_html__( 'Enabled Providers', 'two-factor' ) . '</h2>';
echo '<p class="description">' . esc_html__( 'Choose which Two-Factor providers are available on this site. All providers are enabled by default.', 'two-factor' ) . '</p>';
echo '<form method="post" action="">';
wp_nonce_field( 'two_factor_save_settings', 'two_factor_settings_nonce' );
echo '<fieldset class="two-factor-providers"><legend class="screen-reader-text">' . esc_html__( 'Providers', 'two-factor' ) . '</legend>';
echo '<table class="form-table"><tbody>';
if ( empty( $provider_instances ) ) {
echo '<tr><td>' . esc_html__( 'No providers found.', 'two-factor' ) . '</td></tr>';
} else {
// Render a compact stacked list of provider checkboxes below the title/description.
echo '<tr>';
echo '<td>';
foreach ( $provider_instances as $provider_key => $instance ) {
$label = method_exists( $instance, 'get_label' ) ? $instance->get_label() : $provider_key;
echo '<p class="provider-item"><label for="provider_' . esc_attr( $provider_key ) . '">';
echo '<input type="checkbox" name="two_factor_enabled_providers[]" id="provider_' . esc_attr( $provider_key ) . '" value="' . esc_attr( $provider_key ) . '" ' . checked( in_array( $provider_key, (array) $saved_enabled, true ), true, false ) . ' /> ';
echo esc_html( $label );
echo '</label></p>';
}
echo '</td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</fieldset>';
submit_button( __( 'Save Settings', 'two-factor' ), 'primary', 'two_factor_settings_submit' );
echo '</form>';
echo '</div>';
}
}
@@ -0,0 +1,190 @@
<?php
/**
* Two Factor
*
* @package Two_Factor
* @author WordPress.org Contributors
* @copyright 2020 Plugin Contributors
* @license GPL-2.0-or-later
*
* @wordpress-plugin
* Plugin Name: Two Factor
* Plugin URI: https://wordpress.org/plugins/two-factor/
* Description: Enable Two-Factor Authentication using time-based one-time passwords, email, and backup verification codes.
* Requires at least: 6.8
* Version: 0.16.0
* Requires PHP: 7.2
* Author: WordPress.org Contributors
* Author URI: https://github.com/wordpress/two-factor/graphs/contributors
* License: GPL-2.0-or-later
* License URI: https://spdx.org/licenses/GPL-2.0-or-later.html
* Text Domain: two-factor
* Network: True
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Shortcut constant to the path of this file.
*/
define( 'TWO_FACTOR_DIR', plugin_dir_path( __FILE__ ) );
/**
* Version of the plugin.
*/
define( 'TWO_FACTOR_VERSION', '0.16.0' );
/**
* Include the base class here, so that other plugins can also extend it.
*/
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-provider.php';
/**
* Include the core that handles the common bits.
*/
require_once TWO_FACTOR_DIR . 'class-two-factor-core.php';
/**
* A compatibility layer for some of the most-used plugins out there.
*/
require_once TWO_FACTOR_DIR . 'class-two-factor-compat.php';
// Load settings UI class so the settings page can be rendered.
require_once TWO_FACTOR_DIR . 'settings/class-two-factor-settings.php';
$two_factor_compat = new Two_Factor_Compat();
Two_Factor_Core::add_hooks( $two_factor_compat );
// Delete our options and user meta during uninstall.
register_uninstall_hook( __FILE__, array( Two_Factor_Core::class, 'uninstall' ) );
/**
* Register admin menu and plugin action links.
*
* @since 0.16
*/
function two_factor_register_admin_hooks() {
if ( is_admin() ) {
add_action( 'admin_menu', 'two_factor_add_settings_page' );
}
// Load settings page assets when in admin.
// Settings assets handled inline via standard markup; no extra CSS enqueued.
/* Enforcement filters: restrict providers based on saved enabled-providers option. */
add_filter( 'two_factor_providers', 'two_factor_filter_enabled_providers' );
add_filter( 'two_factor_enabled_providers_for_user', 'two_factor_filter_enabled_providers_for_user', 10, 2 );
}
add_action( 'init', 'two_factor_register_admin_hooks' );
/**
* Add the Two Factor settings page under Settings.
*
* @since 0.16
*/
function two_factor_add_settings_page() {
add_options_page(
__( 'Two-Factor Settings', 'two-factor' ),
__( 'Two-Factor', 'two-factor' ),
'manage_options',
'two-factor-settings',
'two_factor_render_settings_page'
);
}
/**
* Render the settings page via the settings class if available.
*
* @since 0.16
*/
function two_factor_render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Prefer new settings class (keeps main file small).
if ( class_exists( 'Two_Factor_Settings' ) && is_callable( array( 'Two_Factor_Settings', 'render_settings_page' ) ) ) {
Two_Factor_Settings::render_settings_page();
return;
}
// Fallback: no UI available.
echo '<div class="wrap"><h1>' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '</h1>';
echo '<p>' . esc_html__( 'Settings not available.', 'two-factor' ) . '</p></div>';
}
/**
* Helper: retrieve the site-enabled providers option.
* Returns null when the option has never been saved (meaning all providers are allowed).
* Returns an array (possibly empty) when the admin has explicitly saved a selection.
*
* @since 0.16
*
* @return array|null
*/
function two_factor_get_enabled_providers_option() {
$enabled = get_option( 'two_factor_enabled_providers', null );
if ( null === $enabled ) {
return null; // Never saved — allow everything.
}
return is_array( $enabled ) ? $enabled : array();
}
/**
* Filter the registered providers to only those in the site-enabled list.
* This filter receives providers in core format: classname => path.
*
* @since 0.16
*
* @param array $providers Registered providers in classname => path format.
* @return array Filtered list of enabled providers.
*/
function two_factor_filter_enabled_providers( $providers ) {
$site_enabled = two_factor_get_enabled_providers_option();
// null means the option was never saved — allow all providers.
if ( null === $site_enabled ) {
return $providers;
}
// On the settings page itself, show all providers so admins can change the selection.
if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) {
return $providers;
}
foreach ( $providers as $key => $path ) {
if ( ! in_array( $key, $site_enabled, true ) ) {
unset( $providers[ $key ] );
}
}
return $providers;
}
/**
* Filter enabled providers for a user (classnames array) to enforce the site-enabled list.
*
* @since 0.16
*
* @param array $enabled Enabled provider classnames for the user.
* @param int $user_id ID of the user being filtered.
* @return array Filtered list of provider classnames allowed by the site.
*/
function two_factor_filter_enabled_providers_for_user( $enabled, $user_id ) {
$site_enabled = two_factor_get_enabled_providers_option();
// null means the option was never saved — allow all.
if ( null === $site_enabled ) {
return $enabled;
}
return array_values( array_intersect( (array) $enabled, $site_enabled ) );
}
@@ -0,0 +1,76 @@
.two-factor-methods-table tbody th,
.two-factor-methods-table tbody td {
vertical-align: top;
}
.two-factor-methods-table .two-factor-method-label {
display: block;
font-weight: 700;
}
.two-factor-methods-table .two-factor-method-recommended {
font-size: 0.8rem;
line-height: 1;
font-weight: 400;
border: 1px dotted;
border-radius: 0.15rem;
padding: 0.1rem 0.25rem;
margin: 0 0.15rem;
}
#login .backup-methods-wrap {
margin-top: 16px;
padding: 0 24px;
}
#login .backup-methods-wrap a {
text-decoration: none;
}
#login .backup-methods-wrap ul {
list-style-position: inside;
}
/* Prevent Jetpack from hiding our controls, see https://github.com/Automattic/jetpack/issues/3747 */
.jetpack-sso-form-display #loginform > p,
.jetpack-sso-form-display #loginform > div {
display: block;
}
#login form p.two-factor-prompt {
margin-bottom: 1em;
}
#loginform .input.authcode {
letter-spacing: 0.3em;
}
#loginform .input.authcode::placeholder {
opacity: 0.5;
}
.two-factor-backup-codes-wrapper > :not(:last-child) {
margin-bottom: 1em;
}
.two-factor-backup-codes-list-wrap {
background-color: #ddd;
display: inline-block;
margin-top: 24px;
padding: 20px;
}
.two-factor-backup-codes-list-wrap .two-factor-backup-codes-unused-codes {
margin: 0;
padding: revert;
}
.two-factor-backup-codes-list-wrap .two-factor-backup-codes-token {
letter-spacing: 0.3em;
font-family: monospace;
}
#two-factor-qr-code {
min-width: 205px;
min-height: 205px;
}