Two-Factor - Version 0.7.3

Version Description

Download this release

Release Info

Developer kasparsd
Plugin Icon 128x128 Two-Factor
Version 0.7.3
Comparing to
See all releases

Code changes from version 0.4.2 to 0.7.3

LICENSE.md ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 2, June 1991
3
+
4
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6
+ Everyone is permitted to copy and distribute verbatim copies
7
+ of this license document, but changing it is not allowed.
8
+
9
+ Preamble
10
+
11
+ The licenses for most software are designed to take away your
12
+ freedom to share and change it. By contrast, the GNU General Public
13
+ License is intended to guarantee your freedom to share and change free
14
+ software--to make sure the software is free for all its users. This
15
+ General Public License applies to most of the Free Software
16
+ Foundation's software and to any other program whose authors commit to
17
+ using it. (Some other Free Software Foundation software is covered by
18
+ the GNU Lesser General Public License instead.) You can apply it to
19
+ your programs, too.
20
+
21
+ When we speak of free software, we are referring to freedom, not
22
+ price. Our General Public Licenses are designed to make sure that you
23
+ have the freedom to distribute copies of free software (and charge for
24
+ this service if you wish), that you receive source code or can get it
25
+ if you want it, that you can change the software or use pieces of it
26
+ in new free programs; and that you know you can do these things.
27
+
28
+ To protect your rights, we need to make restrictions that forbid
29
+ anyone to deny you these rights or to ask you to surrender the rights.
30
+ These restrictions translate to certain responsibilities for you if you
31
+ distribute copies of the software, or if you modify it.
32
+
33
+ For example, if you distribute copies of such a program, whether
34
+ gratis or for a fee, you must give the recipients all the rights that
35
+ you have. You must make sure that they, too, receive or can get the
36
+ source code. And you must show them these terms so they know their
37
+ rights.
38
+
39
+ We protect your rights with two steps: (1) copyright the software, and
40
+ (2) offer you this license which gives you legal permission to copy,
41
+ distribute and/or modify the software.
42
+
43
+ Also, for each author's protection and ours, we want to make certain
44
+ that everyone understands that there is no warranty for this free
45
+ software. If the software is modified by someone else and passed on, we
46
+ want its recipients to know that what they have is not the original, so
47
+ that any problems introduced by others will not reflect on the original
48
+ authors' reputations.
49
+
50
+ Finally, any free program is threatened constantly by software
51
+ patents. We wish to avoid the danger that redistributors of a free
52
+ program will individually obtain patent licenses, in effect making the
53
+ program proprietary. To prevent this, we have made it clear that any
54
+ patent must be licensed for everyone's free use or not licensed at all.
55
+
56
+ The precise terms and conditions for copying, distribution and
57
+ modification follow.
58
+
59
+ GNU GENERAL PUBLIC LICENSE
60
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61
+
62
+ 0. This License applies to any program or other work which contains
63
+ a notice placed by the copyright holder saying it may be distributed
64
+ under the terms of this General Public License. The "Program", below,
65
+ refers to any such program or work, and a "work based on the Program"
66
+ means either the Program or any derivative work under copyright law:
67
+ that is to say, a work containing the Program or a portion of it,
68
+ either verbatim or with modifications and/or translated into another
69
+ language. (Hereinafter, translation is included without limitation in
70
+ the term "modification".) Each licensee is addressed as "you".
71
+
72
+ Activities other than copying, distribution and modification are not
73
+ covered by this License; they are outside its scope. The act of
74
+ running the Program is not restricted, and the output from the Program
75
+ is covered only if its contents constitute a work based on the
76
+ Program (independent of having been made by running the Program).
77
+ Whether that is true depends on what the Program does.
78
+
79
+ 1. You may copy and distribute verbatim copies of the Program's
80
+ source code as you receive it, in any medium, provided that you
81
+ conspicuously and appropriately publish on each copy an appropriate
82
+ copyright notice and disclaimer of warranty; keep intact all the
83
+ notices that refer to this License and to the absence of any warranty;
84
+ and give any other recipients of the Program a copy of this License
85
+ along with the Program.
86
+
87
+ You may charge a fee for the physical act of transferring a copy, and
88
+ you may at your option offer warranty protection in exchange for a fee.
89
+
90
+ 2. You may modify your copy or copies of the Program or any portion
91
+ of it, thus forming a work based on the Program, and copy and
92
+ distribute such modifications or work under the terms of Section 1
93
+ above, provided that you also meet all of these conditions:
94
+
95
+ a) You must cause the modified files to carry prominent notices
96
+ stating that you changed the files and the date of any change.
97
+
98
+ b) You must cause any work that you distribute or publish, that in
99
+ whole or in part contains or is derived from the Program or any
100
+ part thereof, to be licensed as a whole at no charge to all third
101
+ parties under the terms of this License.
102
+
103
+ c) If the modified program normally reads commands interactively
104
+ when run, you must cause it, when started running for such
105
+ interactive use in the most ordinary way, to print or display an
106
+ announcement including an appropriate copyright notice and a
107
+ notice that there is no warranty (or else, saying that you provide
108
+ a warranty) and that users may redistribute the program under
109
+ these conditions, and telling the user how to view a copy of this
110
+ License. (Exception: if the Program itself is interactive but
111
+ does not normally print such an announcement, your work based on
112
+ the Program is not required to print an announcement.)
113
+
114
+ These requirements apply to the modified work as a whole. If
115
+ identifiable sections of that work are not derived from the Program,
116
+ and can be reasonably considered independent and separate works in
117
+ themselves, then this License, and its terms, do not apply to those
118
+ sections when you distribute them as separate works. But when you
119
+ distribute the same sections as part of a whole which is a work based
120
+ on the Program, the distribution of the whole must be on the terms of
121
+ this License, whose permissions for other licensees extend to the
122
+ entire whole, and thus to each and every part regardless of who wrote it.
123
+
124
+ Thus, it is not the intent of this section to claim rights or contest
125
+ your rights to work written entirely by you; rather, the intent is to
126
+ exercise the right to control the distribution of derivative or
127
+ collective works based on the Program.
128
+
129
+ In addition, mere aggregation of another work not based on the Program
130
+ with the Program (or with a work based on the Program) on a volume of
131
+ a storage or distribution medium does not bring the other work under
132
+ the scope of this License.
133
+
134
+ 3. You may copy and distribute the Program (or a work based on it,
135
+ under Section 2) in object code or executable form under the terms of
136
+ Sections 1 and 2 above provided that you also do one of the following:
137
+
138
+ a) Accompany it with the complete corresponding machine-readable
139
+ source code, which must be distributed under the terms of Sections
140
+ 1 and 2 above on a medium customarily used for software interchange; or,
141
+
142
+ b) Accompany it with a written offer, valid for at least three
143
+ years, to give any third party, for a charge no more than your
144
+ cost of physically performing source distribution, a complete
145
+ machine-readable copy of the corresponding source code, to be
146
+ distributed under the terms of Sections 1 and 2 above on a medium
147
+ customarily used for software interchange; or,
148
+
149
+ c) Accompany it with the information you received as to the offer
150
+ to distribute corresponding source code. (This alternative is
151
+ allowed only for noncommercial distribution and only if you
152
+ received the program in object code or executable form with such
153
+ an offer, in accord with Subsection b above.)
154
+
155
+ The source code for a work means the preferred form of the work for
156
+ making modifications to it. For an executable work, complete source
157
+ code means all the source code for all modules it contains, plus any
158
+ associated interface definition files, plus the scripts used to
159
+ control compilation and installation of the executable. However, as a
160
+ special exception, the source code distributed need not include
161
+ anything that is normally distributed (in either source or binary
162
+ form) with the major components (compiler, kernel, and so on) of the
163
+ operating system on which the executable runs, unless that component
164
+ itself accompanies the executable.
165
+
166
+ If distribution of executable or object code is made by offering
167
+ access to copy from a designated place, then offering equivalent
168
+ access to copy the source code from the same place counts as
169
+ distribution of the source code, even though third parties are not
170
+ compelled to copy the source along with the object code.
171
+
172
+ 4. You may not copy, modify, sublicense, or distribute the Program
173
+ except as expressly provided under this License. Any attempt
174
+ otherwise to copy, modify, sublicense or distribute the Program is
175
+ void, and will automatically terminate your rights under this License.
176
+ However, parties who have received copies, or rights, from you under
177
+ this License will not have their licenses terminated so long as such
178
+ parties remain in full compliance.
179
+
180
+ 5. You are not required to accept this License, since you have not
181
+ signed it. However, nothing else grants you permission to modify or
182
+ distribute the Program or its derivative works. These actions are
183
+ prohibited by law if you do not accept this License. Therefore, by
184
+ modifying or distributing the Program (or any work based on the
185
+ Program), you indicate your acceptance of this License to do so, and
186
+ all its terms and conditions for copying, distributing or modifying
187
+ the Program or works based on it.
188
+
189
+ 6. Each time you redistribute the Program (or any work based on the
190
+ Program), the recipient automatically receives a license from the
191
+ original licensor to copy, distribute or modify the Program subject to
192
+ these terms and conditions. You may not impose any further
193
+ restrictions on the recipients' exercise of the rights granted herein.
194
+ You are not responsible for enforcing compliance by third parties to
195
+ this License.
196
+
197
+ 7. If, as a consequence of a court judgment or allegation of patent
198
+ infringement or for any other reason (not limited to patent issues),
199
+ conditions are imposed on you (whether by court order, agreement or
200
+ otherwise) that contradict the conditions of this License, they do not
201
+ excuse you from the conditions of this License. If you cannot
202
+ distribute so as to satisfy simultaneously your obligations under this
203
+ License and any other pertinent obligations, then as a consequence you
204
+ may not distribute the Program at all. For example, if a patent
205
+ license would not permit royalty-free redistribution of the Program by
206
+ all those who receive copies directly or indirectly through you, then
207
+ the only way you could satisfy both it and this License would be to
208
+ refrain entirely from distribution of the Program.
209
+
210
+ If any portion of this section is held invalid or unenforceable under
211
+ any particular circumstance, the balance of the section is intended to
212
+ apply and the section as a whole is intended to apply in other
213
+ circumstances.
214
+
215
+ It is not the purpose of this section to induce you to infringe any
216
+ patents or other property right claims or to contest validity of any
217
+ such claims; this section has the sole purpose of protecting the
218
+ integrity of the free software distribution system, which is
219
+ implemented by public license practices. Many people have made
220
+ generous contributions to the wide range of software distributed
221
+ through that system in reliance on consistent application of that
222
+ system; it is up to the author/donor to decide if he or she is willing
223
+ to distribute software through any other system and a licensee cannot
224
+ impose that choice.
225
+
226
+ This section is intended to make thoroughly clear what is believed to
227
+ be a consequence of the rest of this License.
228
+
229
+ 8. If the distribution and/or use of the Program is restricted in
230
+ certain countries either by patents or by copyrighted interfaces, the
231
+ original copyright holder who places the Program under this License
232
+ may add an explicit geographical distribution limitation excluding
233
+ those countries, so that distribution is permitted only in or among
234
+ countries not thus excluded. In such case, this License incorporates
235
+ the limitation as if written in the body of this License.
236
+
237
+ 9. The Free Software Foundation may publish revised and/or new versions
238
+ of the General Public License from time to time. Such new versions will
239
+ be similar in spirit to the present version, but may differ in detail to
240
+ address new problems or concerns.
241
+
242
+ Each version is given a distinguishing version number. If the Program
243
+ specifies a version number of this License which applies to it and "any
244
+ later version", you have the option of following the terms and conditions
245
+ either of that version or of any later version published by the Free
246
+ Software Foundation. If the Program does not specify a version number of
247
+ this License, you may choose any version ever published by the Free Software
248
+ Foundation.
249
+
250
+ 10. If you wish to incorporate parts of the Program into other free
251
+ programs whose distribution conditions are different, write to the author
252
+ to ask for permission. For software which is copyrighted by the Free
253
+ Software Foundation, write to the Free Software Foundation; we sometimes
254
+ make exceptions for this. Our decision will be guided by the two goals
255
+ of preserving the free status of all derivatives of our free software and
256
+ of promoting the sharing and reuse of software generally.
257
+
258
+ NO WARRANTY
259
+
260
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261
+ FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263
+ PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264
+ OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266
+ TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267
+ PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268
+ REPAIR OR CORRECTION.
269
+
270
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272
+ REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273
+ INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274
+ OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275
+ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276
+ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277
+ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278
+ POSSIBILITY OF SUCH DAMAGES.
279
+
280
+ END OF TERMS AND CONDITIONS
assets/banner-1544x500.png ADDED
Binary file
assets/banner-772x250.png ADDED
Binary file
assets/icon-128x128.png ADDED
Binary file
assets/icon-256x256.png ADDED
Binary file
assets/icon.svg ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
2
+ <g fill="none" fill-rule="evenodd">
3
+ <path fill="#CCC" d="M98 150a60 60 0 1 1 60 0v60a8 8 0 0 1-8 8h-44a8 8 0 0 1-8-8v-60z"/>
4
+ <path fill="#0073AA" d="M116 132a36 36 0 1 1 24 0v64.7a4 4 0 0 1-4 4h-16a4 4 0 0 1-4-4v-64-.7z"/>
5
+ </g>
6
+ </svg>
assets/screenshot-1.png ADDED
Binary file
assets/screenshot-2.png ADDED
Binary file
assets/screenshot-3.png ADDED
Binary file
class-two-factor-compat.php ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * A compatibility layer for some of the most popular plugins.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
+ /**
9
+ * A compatibility layer for some of the most popular plugins.
10
+ *
11
+ * Should be used with care because ideally we wouldn't need
12
+ * any integration specific code for this plugin. Everything should
13
+ * be handled through clever use of hooks and best practices.
14
+ */
15
+ class Two_Factor_Compat {
16
+ /**
17
+ * Initialize all the custom hooks as necessary.
18
+ *
19
+ * @return void
20
+ */
21
+ public function init() {
22
+ /**
23
+ * Jetpack
24
+ *
25
+ * @see https://wordpress.org/plugins/jetpack/
26
+ */
27
+ add_filter( 'two_factor_rememberme', array( $this, 'jetpack_rememberme' ) );
28
+ }
29
+
30
+ /**
31
+ * Jetpack single sign-on wants long-lived sessions for users.
32
+ *
33
+ * @param boolean $rememberme Current state of the "remember me" toggle.
34
+ *
35
+ * @return boolean
36
+ */
37
+ public function jetpack_rememberme( $rememberme ) {
38
+ $action = filter_input( INPUT_GET, 'action', FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
39
+
40
+ if ( 'jetpack-sso' === $action && $this->jetpack_is_sso_active() ) {
41
+ return true;
42
+ }
43
+
44
+ return $rememberme;
45
+ }
46
+
47
+ /**
48
+ * Helper to detect the presence of the active SSO module.
49
+ *
50
+ * @return boolean
51
+ */
52
+ public function jetpack_is_sso_active() {
53
+ return ( method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'sso' ) );
54
+ }
55
+ }
class.two-factor-core.php → class-two-factor-core.php RENAMED
@@ -1,4 +1,10 @@
1
  <?php
 
 
 
 
 
 
2
  /**
3
  * Class for creating two factor authorization.
4
  *
@@ -27,14 +33,38 @@ class Two_Factor_Core {
27
  *
28
  * @type string
29
  */
30
- const USER_META_NONCE_KEY = '_two_factor_nonce';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  /**
33
  * Set up filters and actions.
34
  *
 
 
35
  * @since 0.1-dev
36
  */
37
- public static function add_hooks() {
38
  add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) );
39
  add_action( 'init', array( __CLASS__, 'get_providers' ) );
40
  add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 );
@@ -48,8 +78,22 @@ class Two_Factor_Core {
48
  add_filter( 'wpmu_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) );
49
  add_filter( 'manage_users_custom_column', array( __CLASS__, 'manage_users_custom_column' ), 10, 3 );
50
 
 
 
 
 
 
 
 
 
 
51
  // Run only after the core wp_authenticate_username_password() check.
52
  add_filter( 'authenticate', array( __CLASS__, 'filter_authenticate' ), 50 );
 
 
 
 
 
53
  }
54
 
55
  /**
@@ -70,11 +114,11 @@ class Two_Factor_Core {
70
  */
71
  public static function get_providers() {
72
  $providers = array(
73
- 'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class.two-factor-email.php',
74
- 'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class.two-factor-totp.php',
75
- 'Two_Factor_FIDO_U2F' => TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f.php',
76
- 'Two_Factor_Backup_Codes' => TWO_FACTOR_DIR . 'providers/class.two-factor-backup-codes.php',
77
- 'Two_Factor_Dummy' => TWO_FACTOR_DIR . 'providers/class.two-factor-dummy.php',
78
  );
79
 
80
  /**
@@ -91,18 +135,20 @@ class Two_Factor_Core {
91
  // FIDO U2F is PHP 5.3+ only.
92
  if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) {
93
  unset( $providers['Two_Factor_FIDO_U2F'] );
94
- trigger_error( sprintf( // WPCS: XSS OK.
 
95
  /* translators: %s: version number */
96
- __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ),
97
- PHP_VERSION
98
- ) );
 
99
  }
100
 
101
  /**
102
  * For each filtered provider,
103
  */
104
  foreach ( $providers as $class => $path ) {
105
- include_once( $path );
106
 
107
  /**
108
  * Confirm that it's been successfully included before instantiating.
@@ -119,6 +165,149 @@ class Two_Factor_Core {
119
  return $providers;
120
  }
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  /**
123
  * Get all Two-Factor Auth providers that are enabled for the specified|current user.
124
  *
@@ -137,7 +326,13 @@ class Two_Factor_Core {
137
  }
138
  $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
139
 
140
- return $enabled_providers;
 
 
 
 
 
 
141
  }
142
 
143
  /**
@@ -156,7 +351,7 @@ class Two_Factor_Core {
156
  $configured_providers = array();
157
 
158
  foreach ( $providers as $classname => $provider ) {
159
- if ( in_array( $classname, $enabled_providers ) && $provider->is_available_for_user( $user ) ) {
160
  $configured_providers[ $classname ] = $provider;
161
  }
162
  }
@@ -235,12 +430,35 @@ class Two_Factor_Core {
235
  return;
236
  }
237
 
 
 
 
 
238
  wp_clear_auth_cookie();
239
 
240
  self::show_two_factor_login( $user );
241
  exit;
242
  }
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  /**
245
  * Prevent login through XML-RPC and REST API for users with at least one
246
  * two-factor method enabled.
@@ -318,29 +536,33 @@ class Two_Factor_Core {
318
  * @since 0.1-dev
319
  */
320
  public static function backup_2fa() {
321
- if ( ! isset( $_GET['wp-auth-id'], $_GET['wp-auth-nonce'], $_GET['provider'] ) ) {
 
 
 
 
322
  return;
323
  }
324
 
325
- $user = get_userdata( $_GET['wp-auth-id'] );
326
  if ( ! $user ) {
327
  return;
328
  }
329
 
330
- $nonce = $_GET['wp-auth-nonce'];
331
  if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) {
332
- wp_safe_redirect( get_bloginfo( 'url' ) );
333
  exit;
334
  }
335
 
336
  $providers = self::get_available_providers_for_user( $user );
337
- if ( isset( $providers[ $_GET['provider'] ] ) ) {
338
- $provider = $providers[ $_GET['provider'] ];
339
  } else {
340
- wp_die( esc_html__( 'Cheatin&#8217; uh?' ), 403 );
341
  }
342
 
343
- self::login_html( $user, $_GET['wp-auth-nonce'], $_GET['redirect_to'], '', $provider );
 
344
 
345
  exit;
346
  }
@@ -366,17 +588,14 @@ class Two_Factor_Core {
366
  $provider_class = get_class( $provider );
367
 
368
  $available_providers = self::get_available_providers_for_user( $user );
369
- $backup_providers = array_diff_key( $available_providers, array( $provider_class => null ) );
370
- $interim_login = isset( $_REQUEST['interim-login'] ); // WPCS: CSRF ok.
371
 
372
- $rememberme = 0;
373
- if ( isset( $_REQUEST['rememberme'] ) && $_REQUEST['rememberme'] ) {
374
- $rememberme = 1;
375
- }
376
 
377
  if ( ! function_exists( 'login_header' ) ) {
378
  // We really should migrate login_header() out of `wp-login.php` so it can be called from an includes file.
379
- include_once( TWO_FACTOR_DIR . 'includes/function.login-header.php' );
380
  }
381
 
382
  login_header();
@@ -390,11 +609,11 @@ class Two_Factor_Core {
390
  <input type="hidden" name="provider" id="provider" value="<?php echo esc_attr( $provider_class ); ?>" />
391
  <input type="hidden" name="wp-auth-id" id="wp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>" />
392
  <input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" value="<?php echo esc_attr( $login_nonce ); ?>" />
393
- <?php if ( $interim_login ) { ?>
394
  <input type="hidden" name="interim-login" value="1" />
395
- <?php } else { ?>
396
  <input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" />
397
- <?php } ?>
398
  <input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr( $rememberme ); ?>" />
399
 
400
  <?php $provider->authentication_page( $user ); ?>
@@ -403,8 +622,8 @@ class Two_Factor_Core {
403
  <?php
404
  if ( 1 === count( $backup_providers ) ) :
405
  $backup_classname = key( $backup_providers );
406
- $backup_provider = $backup_providers[ $backup_classname ];
407
- $login_url = self::login_url(
408
  array(
409
  'action' => 'backup_2fa',
410
  'provider' => $backup_classname,
@@ -453,55 +672,41 @@ class Two_Factor_Core {
453
  ?>
454
  <li>
455
  <a href="<?php echo esc_url( $login_url ); ?>">
456
- <?php $backup_provider->print_label(); ?>
457
  </a>
458
  </li>
459
  <?php endforeach; ?>
460
  </ul>
461
  </div>
462
  <?php endif; ?>
463
-
464
- <p id="backtoblog">
465
- <a href="<?php echo esc_url( home_url( '/' ) ); ?>" title="<?php esc_attr_e( 'Are you lost?', 'two-factor' ); ?>">
466
- <?php
467
- echo esc_html(
468
- sprintf(
469
- // translators: %s: site name.
470
- __( '&larr; Back to %s', 'two-factor' ),
471
- get_bloginfo( 'title', 'display' )
472
- )
473
- );
474
- ?>
475
- </a>
476
- </p>
477
-
478
  <style>
479
- /* @todo: migrate to an external stylesheet. */
480
- .backup-methods-wrap {
481
  margin-top: 16px;
482
  padding: 0 24px;
483
- }
484
- .backup-methods-wrap a {
485
  color: #999;
486
  text-decoration: none;
487
- }
488
- ul.backup-methods {
489
  display: none;
490
  padding-left: 1.5em;
491
- }
492
- /* Prevent Jetpack from hiding our controls, see https://github.com/Automattic/jetpack/issues/3747 */
493
- .jetpack-sso-form-display #loginform > p,
494
- .jetpack-sso-form-display #loginform > div {
495
  display: block;
496
- }
497
  </style>
498
 
499
  <?php
500
- /** This action is documented in wp-login.php */
501
- do_action( 'login_footer' ); ?>
502
- <div class="clear"></div>
503
- </body>
504
- </html>
 
505
  <?php
506
  }
507
 
@@ -523,6 +728,17 @@ class Two_Factor_Core {
523
  return add_query_arg( $params, site_url( 'wp-login.php', $scheme ) );
524
  }
525
 
 
 
 
 
 
 
 
 
 
 
 
526
  /**
527
  * Create the login nonce.
528
  *
@@ -532,15 +748,21 @@ class Two_Factor_Core {
532
  * @return array
533
  */
534
  public static function create_login_nonce( $user_id ) {
535
- $login_nonce = array();
 
 
 
536
  try {
537
  $login_nonce['key'] = bin2hex( random_bytes( 32 ) );
538
- } catch (Exception $ex) {
539
- $login_nonce['key'] = wp_hash( $user_id . mt_rand() . microtime(), 'nonce' );
540
  }
541
- $login_nonce['expiration'] = time() + HOUR_IN_SECONDS;
542
 
543
- if ( ! update_user_meta( $user_id, self::USER_META_NONCE_KEY, $login_nonce ) ) {
 
 
 
 
544
  return false;
545
  }
546
 
@@ -570,16 +792,19 @@ class Two_Factor_Core {
570
  */
571
  public static function verify_login_nonce( $user_id, $nonce ) {
572
  $login_nonce = get_user_meta( $user_id, self::USER_META_NONCE_KEY, true );
573
- if ( ! $login_nonce ) {
 
574
  return false;
575
  }
576
 
577
- if ( $nonce !== $login_nonce['key'] || time() > $login_nonce['expiration'] ) {
578
- self::delete_login_nonce( $user_id );
579
- return false;
580
  }
581
 
582
- return true;
 
 
 
583
  }
584
 
585
  /**
@@ -588,27 +813,30 @@ class Two_Factor_Core {
588
  * @since 0.1-dev
589
  */
590
  public static function login_form_validate_2fa() {
591
- if ( ! isset( $_POST['wp-auth-id'], $_POST['wp-auth-nonce'] ) ) {
 
 
 
592
  return;
593
  }
594
 
595
- $user = get_userdata( $_POST['wp-auth-id'] );
596
  if ( ! $user ) {
597
  return;
598
  }
599
 
600
- $nonce = $_POST['wp-auth-nonce'];
601
  if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) {
602
- wp_safe_redirect( get_bloginfo( 'url' ) );
603
  exit;
604
  }
605
 
606
- if ( isset( $_POST['provider'] ) ) {
 
607
  $providers = self::get_available_providers_for_user( $user );
608
- if ( isset( $providers[ $_POST['provider'] ] ) ) {
609
- $provider = $providers[ $_POST['provider'] ];
610
  } else {
611
- wp_die( esc_html__( 'Cheatin&#8217; uh?' ), 403 );
612
  }
613
  } else {
614
  $provider = self::get_primary_provider_for_user( $user->ID );
@@ -627,7 +855,7 @@ class Two_Factor_Core {
627
 
628
  // Ask the provider to verify the second factor.
629
  if ( true !== $provider->validate_authentication( $user ) ) {
630
- do_action( 'wp_login_failed', $user->user_login );
631
 
632
  $login_nonce = self::create_login_nonce( $user->ID );
633
  if ( ! $login_nonce ) {
@@ -647,9 +875,11 @@ class Two_Factor_Core {
647
 
648
  wp_set_auth_cookie( $user->ID, $rememberme );
649
 
 
 
650
  // Must be global because that's how login_header() uses it.
651
  global $interim_login;
652
- $interim_login = isset( $_REQUEST['interim-login'] ); // WPCS: override ok.
653
 
654
  if ( $interim_login ) {
655
  $customize_login = isset( $_REQUEST['customize-login'] );
@@ -657,14 +887,16 @@ class Two_Factor_Core {
657
  wp_enqueue_script( 'customize-base' );
658
  }
659
  $message = '<p class="message">' . __( 'You have logged in successfully.', 'two-factor' ) . '</p>';
660
- $interim_login = 'success'; // WPCS: override ok.
661
- login_header( '', $message ); ?>
 
662
  </div>
663
  <?php
664
  /** This action is documented in wp-login.php */
665
- do_action( 'login_footer' ); ?>
 
666
  <?php if ( $customize_login ) : ?>
667
- <script type="text/javascript">setTimeout( function(){ new wp.customize.Messenger({ url: '<?php echo wp_customize_url(); /* WPCS: XSS OK. */ ?>', channel: 'login' }).send('login') }, 1000 );</script>
668
  <?php endif; ?>
669
  </body></html>
670
  <?php
@@ -683,7 +915,7 @@ class Two_Factor_Core {
683
  * @return array Updated array of columns.
684
  */
685
  public static function filter_manage_users_columns( array $columns ) {
686
- $columns['two-factor'] = __( 'Two-Factor' );
687
  return $columns;
688
  }
689
 
@@ -720,10 +952,10 @@ class Two_Factor_Core {
720
  * @param WP_User $user WP_User object of the logged-in user.
721
  */
722
  public static function user_two_factor_options( $user ) {
723
- wp_enqueue_style( 'user-edit-2fa', plugins_url( 'user-edit.css', __FILE__ ) );
724
 
725
  $enabled_providers = array_keys( self::get_available_providers_for_user( $user ) );
726
- $primary_provider = self::get_primary_provider_for_user( $user->ID );
727
 
728
  if ( ! empty( $primary_provider ) && is_object( $primary_provider ) ) {
729
  $primary_provider_key = get_class( $primary_provider );
@@ -735,10 +967,10 @@ class Two_Factor_Core {
735
 
736
  ?>
737
  <input type="hidden" name="<?php echo esc_attr( self::ENABLED_PROVIDERS_USER_META_KEY ); ?>[]" value="<?php /* Dummy input so $_POST value is passed when no providers are enabled. */ ?>" />
738
- <table class="form-table">
739
  <tr>
740
  <th>
741
- <?php esc_html_e( 'Two-Factor Options' ); ?>
742
  </th>
743
  <td>
744
  <table class="two-factor-methods-table">
@@ -746,17 +978,29 @@ class Two_Factor_Core {
746
  <tr>
747
  <th class="col-enabled" scope="col"><?php esc_html_e( 'Enabled', 'two-factor' ); ?></th>
748
  <th class="col-primary" scope="col"><?php esc_html_e( 'Primary', 'two-factor' ); ?></th>
749
- <th class="col-name" scope="col"><?php esc_html_e( 'Name', 'two-factor' ); ?></th>
750
  </tr>
751
  </thead>
752
  <tbody>
753
  <?php foreach ( self::get_providers() as $class => $object ) : ?>
754
  <tr>
755
- <th scope="row"><input type="checkbox" name="<?php echo esc_attr( self::ENABLED_PROVIDERS_USER_META_KEY ); ?>[]" value="<?php echo esc_attr( $class ); ?>" <?php checked( in_array( $class, $enabled_providers ) ); ?> /></th>
756
  <th scope="row"><input type="radio" name="<?php echo esc_attr( self::PROVIDER_USER_META_KEY ); ?>" value="<?php echo esc_attr( $class ); ?>" <?php checked( $class, $primary_provider_key ); ?> /></th>
757
  <td>
758
- <?php $object->print_label(); ?>
759
- <?php do_action( 'two-factor-user-options-' . $class, $user ); ?>
 
 
 
 
 
 
 
 
 
 
 
 
760
  </td>
761
  </tr>
762
  <?php endforeach; ?>
@@ -809,4 +1053,19 @@ class Two_Factor_Core {
809
  }
810
  }
811
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  }
1
  <?php
2
+ /**
3
+ * Two Factore Core Class.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
  /**
9
  * Class for creating two factor authorization.
10
  *
33
  *
34
  * @type string
35
  */
36
+ const USER_META_NONCE_KEY = '_two_factor_nonce';
37
+
38
+ /**
39
+ * URL query paramater used for our custom actions.
40
+ *
41
+ * @var string
42
+ */
43
+ const USER_SETTINGS_ACTION_QUERY_VAR = 'two_factor_action';
44
+
45
+ /**
46
+ * Nonce key for user settings.
47
+ *
48
+ * @var string
49
+ */
50
+ const USER_SETTINGS_ACTION_NONCE_QUERY_ARG = '_two_factor_action_nonce';
51
+
52
+ /**
53
+ * Keep track of all the password-based authentication sessions that
54
+ * need to invalidated before the second factor authentication.
55
+ *
56
+ * @var array
57
+ */
58
+ private static $password_auth_tokens = array();
59
 
60
  /**
61
  * Set up filters and actions.
62
  *
63
+ * @param object $compat A compaitbility later for plugins.
64
+ *
65
  * @since 0.1-dev
66
  */
67
+ public static function add_hooks( $compat ) {
68
  add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) );
69
  add_action( 'init', array( __CLASS__, 'get_providers' ) );
70
  add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 );
78
  add_filter( 'wpmu_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) );
79
  add_filter( 'manage_users_custom_column', array( __CLASS__, 'manage_users_custom_column' ), 10, 3 );
80
 
81
+ /**
82
+ * Keep track of all the user sessions for which we need to invalidate the
83
+ * authentication cookies set during the initial password check.
84
+ *
85
+ * Is there a better way of doing this?
86
+ */
87
+ add_action( 'set_auth_cookie', array( __CLASS__, 'collect_auth_cookie_tokens' ) );
88
+ add_action( 'set_logged_in_cookie', array( __CLASS__, 'collect_auth_cookie_tokens' ) );
89
+
90
  // Run only after the core wp_authenticate_username_password() check.
91
  add_filter( 'authenticate', array( __CLASS__, 'filter_authenticate' ), 50 );
92
+
93
+ add_action( 'admin_init', array( __CLASS__, 'trigger_user_settings_action' ) );
94
+ add_filter( 'two_factor_providers', array( __CLASS__, 'enable_dummy_method_for_debug' ) );
95
+
96
+ $compat->init();
97
  }
98
 
99
  /**
114
  */
115
  public static function get_providers() {
116
  $providers = array(
117
+ 'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class-two-factor-email.php',
118
+ 'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class-two-factor-totp.php',
119
+ 'Two_Factor_FIDO_U2F' => TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f.php',
120
+ 'Two_Factor_Backup_Codes' => TWO_FACTOR_DIR . 'providers/class-two-factor-backup-codes.php',
121
+ 'Two_Factor_Dummy' => TWO_FACTOR_DIR . 'providers/class-two-factor-dummy.php',
122
  );
123
 
124
  /**
135
  // FIDO U2F is PHP 5.3+ only.
136
  if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) {
137
  unset( $providers['Two_Factor_FIDO_U2F'] );
138
+ trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
139
+ sprintf(
140
  /* translators: %s: version number */
141
+ __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
142
+ PHP_VERSION
143
+ )
144
+ );
145
  }
146
 
147
  /**
148
  * For each filtered provider,
149
  */
150
  foreach ( $providers as $class => $path ) {
151
+ include_once $path;
152
 
153
  /**
154
  * Confirm that it's been successfully included before instantiating.
165
  return $providers;
166
  }
167
 
168
+ /**
169
+ * Enable the dummy method only during debugging.
170
+ *
171
+ * @param array $methods List of enabled methods.
172
+ *
173
+ * @return array
174
+ */
175
+ public static function enable_dummy_method_for_debug( $methods ) {
176
+ if ( ! self::is_wp_debug() ) {
177
+ unset( $methods['Two_Factor_Dummy'] );
178
+ }
179
+
180
+ return $methods;
181
+ }
182
+
183
+ /**
184
+ * Check if the debug mode is enabled.
185
+ *
186
+ * @return boolean
187
+ */
188
+ protected static function is_wp_debug() {
189
+ return ( defined( 'WP_DEBUG' ) && WP_DEBUG );
190
+ }
191
+
192
+ /**
193
+ * Get the user settings page URL.
194
+ *
195
+ * Fetch this from the plugin core after we introduce proper dependency injection
196
+ * and get away from the singletons at the provider level (should be handled by core).
197
+ *
198
+ * @param integer $user_id User ID.
199
+ *
200
+ * @return string
201
+ */
202
+ protected static function get_user_settings_page_url( $user_id ) {
203
+ $page = 'user-edit.php';
204
+
205
+ if ( defined( 'IS_PROFILE_PAGE' ) && IS_PROFILE_PAGE ) {
206
+ $page = 'profile.php';
207
+ }
208
+
209
+ return add_query_arg(
210
+ array(
211
+ 'user_id' => intval( $user_id ),
212
+ ),
213
+ self_admin_url( $page )
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Get the URL for resetting the secret token.
219
+ *
220
+ * @param integer $user_id User ID.
221
+ * @param string $action Custom two factor action key.
222
+ *
223
+ * @return string
224
+ */
225
+ public static function get_user_update_action_url( $user_id, $action ) {
226
+ return wp_nonce_url(
227
+ add_query_arg(
228
+ array(
229
+ self::USER_SETTINGS_ACTION_QUERY_VAR => $action,
230
+ ),
231
+ self::get_user_settings_page_url( $user_id )
232
+ ),
233
+ sprintf( '%d-%s', $user_id, $action ),
234
+ self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Check if a user action is valid.
240
+ *
241
+ * @param integer $user_id User ID.
242
+ * @param string $action User action ID.
243
+ *
244
+ * @return boolean
245
+ */
246
+ public static function is_valid_user_action( $user_id, $action ) {
247
+ $request_nonce = filter_input( INPUT_GET, self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG, FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
248
+
249
+ return wp_verify_nonce(
250
+ $request_nonce,
251
+ sprintf( '%d-%s', $user_id, $action )
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Get the ID of the user being edited.
257
+ *
258
+ * @return integer
259
+ */
260
+ public static function current_user_being_edited() {
261
+ // Try to resolve the user ID from the request first.
262
+ if ( ! empty( $_REQUEST['user_id'] ) ) {
263
+ $user_id = intval( $_REQUEST['user_id'] );
264
+
265
+ if ( current_user_can( 'edit_user', $user_id ) ) {
266
+ return $user_id;
267
+ }
268
+ }
269
+
270
+ return get_current_user_id();
271
+ }
272
+
273
+ /**
274
+ * Trigger our custom update action if a valid
275
+ * action request is detected and passes the nonce check.
276
+ *
277
+ * @return void
278
+ */
279
+ public static function trigger_user_settings_action() {
280
+ $action = filter_input( INPUT_GET, self::USER_SETTINGS_ACTION_QUERY_VAR, FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
281
+ $user_id = self::current_user_being_edited();
282
+
283
+ if ( ! empty( $action ) && self::is_valid_user_action( $user_id, $action ) ) {
284
+ /**
285
+ * This action is triggered when a valid Two Factor settings
286
+ * action is detected and it passes the nonce validation.
287
+ *
288
+ * @param integer $user_id User ID.
289
+ * @param string $action Settings action.
290
+ */
291
+ do_action( 'two_factor_user_settings_action', $user_id, $action );
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Keep track of all the authentication cookies that need to be
297
+ * invalidated before the second factor authentication.
298
+ *
299
+ * @param string $cookie Cookie string.
300
+ *
301
+ * @return void
302
+ */
303
+ public static function collect_auth_cookie_tokens( $cookie ) {
304
+ $parsed = wp_parse_auth_cookie( $cookie );
305
+
306
+ if ( ! empty( $parsed['token'] ) ) {
307
+ self::$password_auth_tokens[] = $parsed['token'];
308
+ }
309
+ }
310
+
311
  /**
312
  * Get all Two-Factor Auth providers that are enabled for the specified|current user.
313
  *
326
  }
327
  $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
328
 
329
+ /**
330
+ * Filter the enabled two-factor authentication providers for this user.
331
+ *
332
+ * @param array $enabled_providers The enabled providers.
333
+ * @param int $user_id The user ID.
334
+ */
335
+ return apply_filters( 'two_factor_enabled_providers_for_user', $enabled_providers, $user->ID );
336
  }
337
 
338
  /**
351
  $configured_providers = array();
352
 
353
  foreach ( $providers as $classname => $provider ) {
354
+ if ( in_array( $classname, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) {
355
  $configured_providers[ $classname ] = $provider;
356
  }
357
  }
430
  return;
431
  }
432
 
433
+ // Invalidate the current login session to prevent from being re-used.
434
+ self::destroy_current_session_for_user( $user );
435
+
436
+ // Also clear the cookies which are no longer valid.
437
  wp_clear_auth_cookie();
438
 
439
  self::show_two_factor_login( $user );
440
  exit;
441
  }
442
 
443
+ /**
444
+ * Destroy the known password-based authentication sessions for the current user.
445
+ *
446
+ * Is there a better way of finding the current session token without
447
+ * having access to the authentication cookies which are just being set
448
+ * on the first password-based authentication request.
449
+ *
450
+ * @param \WP_User $user User object.
451
+ *
452
+ * @return void
453
+ */
454
+ public static function destroy_current_session_for_user( $user ) {
455
+ $session_manager = WP_Session_Tokens::get_instance( $user->ID );
456
+
457
+ foreach ( self::$password_auth_tokens as $auth_token ) {
458
+ $session_manager->destroy( $auth_token );
459
+ }
460
+ }
461
+
462
  /**
463
  * Prevent login through XML-RPC and REST API for users with at least one
464
  * two-factor method enabled.
536
  * @since 0.1-dev
537
  */
538
  public static function backup_2fa() {
539
+ $wp_auth_id = filter_input( INPUT_GET, 'wp-auth-id', FILTER_SANITIZE_NUMBER_INT );
540
+ $nonce = filter_input( INPUT_GET, 'wp-auth-nonce', FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
541
+ $provider = filter_input( INPUT_GET, 'provider', FILTER_CALLBACK, array( 'options' => 'sanitize_text_field' ) );
542
+
543
+ if ( ! $wp_auth_id || ! $nonce || ! $provider ) {
544
  return;
545
  }
546
 
547
+ $user = get_userdata( $wp_auth_id );
548
  if ( ! $user ) {
549
  return;
550
  }
551
 
 
552
  if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) {
553
+ wp_safe_redirect( home_url() );
554
  exit;
555
  }
556
 
557
  $providers = self::get_available_providers_for_user( $user );
558
+ if ( isset( $providers[ $provider ] ) ) {
559
+ $provider = $providers[ $provider ];
560
  } else {
561
+ wp_die( esc_html__( 'Cheatin&#8217; uh?', 'two-factor' ), 403 );
562
  }
563
 
564
+ $redirect_to = filter_input( INPUT_GET, 'redirect_to', FILTER_SANITIZE_URL );
565
+ self::login_html( $user, $nonce, $redirect_to, '', $provider );
566
 
567
  exit;
568
  }
588
  $provider_class = get_class( $provider );
589
 
590
  $available_providers = self::get_available_providers_for_user( $user );
591
+ $backup_providers = array_diff_key( $available_providers, array( $provider_class => null ) );
592
+ $interim_login = isset( $_REQUEST['interim-login'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
593
 
594
+ $rememberme = intval( self::rememberme() );
 
 
 
595
 
596
  if ( ! function_exists( 'login_header' ) ) {
597
  // We really should migrate login_header() out of `wp-login.php` so it can be called from an includes file.
598
+ include_once TWO_FACTOR_DIR . 'includes/function.login-header.php';
599
  }
600
 
601
  login_header();
609
  <input type="hidden" name="provider" id="provider" value="<?php echo esc_attr( $provider_class ); ?>" />
610
  <input type="hidden" name="wp-auth-id" id="wp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>" />
611
  <input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" value="<?php echo esc_attr( $login_nonce ); ?>" />
612
+ <?php if ( $interim_login ) { ?>
613
  <input type="hidden" name="interim-login" value="1" />
614
+ <?php } else { ?>
615
  <input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" />
616
+ <?php } ?>
617
  <input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr( $rememberme ); ?>" />
618
 
619
  <?php $provider->authentication_page( $user ); ?>
622
  <?php
623
  if ( 1 === count( $backup_providers ) ) :
624
  $backup_classname = key( $backup_providers );
625
+ $backup_provider = $backup_providers[ $backup_classname ];
626
+ $login_url = self::login_url(
627
  array(
628
  'action' => 'backup_2fa',
629
  'provider' => $backup_classname,
672
  ?>
673
  <li>
674
  <a href="<?php echo esc_url( $login_url ); ?>">
675
+ <?php echo esc_html( $backup_provider->get_label() ); ?>
676
  </a>
677
  </li>
678
  <?php endforeach; ?>
679
  </ul>
680
  </div>
681
  <?php endif; ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  <style>
683
+ /* @todo: migrate to an external stylesheet. */
684
+ .backup-methods-wrap {
685
  margin-top: 16px;
686
  padding: 0 24px;
687
+ }
688
+ .backup-methods-wrap a {
689
  color: #999;
690
  text-decoration: none;
691
+ }
692
+ ul.backup-methods {
693
  display: none;
694
  padding-left: 1.5em;
695
+ }
696
+ /* Prevent Jetpack from hiding our controls, see https://github.com/Automattic/jetpack/issues/3747 */
697
+ .jetpack-sso-form-display #loginform > p,
698
+ .jetpack-sso-form-display #loginform > div {
699
  display: block;
700
+ }
701
  </style>
702
 
703
  <?php
704
+ if ( ! function_exists( 'login_footer' ) ) {
705
+ include_once TWO_FACTOR_DIR . 'includes/function.login-footer.php';
706
+ }
707
+
708
+ login_footer();
709
+ ?>
710
  <?php
711
  }
712
 
728
  return add_query_arg( $params, site_url( 'wp-login.php', $scheme ) );
729
  }
730
 
731
+ /**
732
+ * Get the hash of a nonce for storage and comparison.
733
+ *
734
+ * @param string $nonce Nonce value to be hashed.
735
+ *
736
+ * @return string
737
+ */
738
+ protected static function hash_login_nonce( $nonce ) {
739
+ return wp_hash( $nonce, 'nonce' );
740
+ }
741
+
742
  /**
743
  * Create the login nonce.
744
  *
748
  * @return array
749
  */
750
  public static function create_login_nonce( $user_id ) {
751
+ $login_nonce = array(
752
+ 'expiration' => time() + HOUR_IN_SECONDS,
753
+ );
754
+
755
  try {
756
  $login_nonce['key'] = bin2hex( random_bytes( 32 ) );
757
+ } catch ( Exception $ex ) {
758
+ $login_nonce['key'] = wp_hash( $user_id . wp_rand() . microtime(), 'nonce' );
759
  }
 
760
 
761
+ // Store the nonce hashed to avoid leaking it via database access.
762
+ $login_nonce_stored = $login_nonce;
763
+ $login_nonce_stored['key'] = self::hash_login_nonce( $login_nonce['key'] );
764
+
765
+ if ( ! update_user_meta( $user_id, self::USER_META_NONCE_KEY, $login_nonce_stored ) ) {
766
  return false;
767
  }
768
 
792
  */
793
  public static function verify_login_nonce( $user_id, $nonce ) {
794
  $login_nonce = get_user_meta( $user_id, self::USER_META_NONCE_KEY, true );
795
+
796
+ if ( ! $login_nonce || empty( $login_nonce['key'] ) || empty( $login_nonce['expiration'] ) ) {
797
  return false;
798
  }
799
 
800
+ if ( hash_equals( $login_nonce['key'], self::hash_login_nonce( $nonce ) ) && time() < $login_nonce['expiration'] ) {
801
+ return true;
 
802
  }
803
 
804
+ // Require a fresh nonce if verification fails.
805
+ self::delete_login_nonce( $user_id );
806
+
807
+ return false;
808
  }
809
 
810
  /**
813
  * @since 0.1-dev
814
  */
815
  public static function login_form_validate_2fa() {
816
+ $wp_auth_id = filter_input( INPUT_POST, 'wp-auth-id', FILTER_SANITIZE_NUMBER_INT );
817
+ $nonce = filter_input( INPUT_POST, 'wp-auth-nonce', FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
818
+
819
+ if ( ! $wp_auth_id || ! $nonce ) {
820
  return;
821
  }
822
 
823
+ $user = get_userdata( $wp_auth_id );
824
  if ( ! $user ) {
825
  return;
826
  }
827
 
 
828
  if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) {
829
+ wp_safe_redirect( home_url() );
830
  exit;
831
  }
832
 
833
+ $provider = filter_input( INPUT_POST, 'provider', FILTER_CALLBACK, array( 'options' => 'sanitize_text_field' ) );
834
+ if ( $provider ) {
835
  $providers = self::get_available_providers_for_user( $user );
836
+ if ( isset( $providers[ $provider ] ) ) {
837
+ $provider = $providers[ $provider ];
838
  } else {
839
+ wp_die( esc_html__( 'Cheatin&#8217; uh?', 'two-factor' ), 403 );
840
  }
841
  } else {
842
  $provider = self::get_primary_provider_for_user( $user->ID );
855
 
856
  // Ask the provider to verify the second factor.
857
  if ( true !== $provider->validate_authentication( $user ) ) {
858
+ do_action( 'wp_login_failed', $user->user_login, new WP_Error( 'two_factor_invalid', __( 'ERROR: Invalid verification code.', 'two-factor' ) ) );
859
 
860
  $login_nonce = self::create_login_nonce( $user->ID );
861
  if ( ! $login_nonce ) {
875
 
876
  wp_set_auth_cookie( $user->ID, $rememberme );
877
 
878
+ do_action( 'two_factor_user_authenticated', $user );
879
+
880
  // Must be global because that's how login_header() uses it.
881
  global $interim_login;
882
+ $interim_login = isset( $_REQUEST['interim-login'] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited,WordPress.Security.NonceVerification.Recommended
883
 
884
  if ( $interim_login ) {
885
  $customize_login = isset( $_REQUEST['customize-login'] );
887
  wp_enqueue_script( 'customize-base' );
888
  }
889
  $message = '<p class="message">' . __( 'You have logged in successfully.', 'two-factor' ) . '</p>';
890
+ $interim_login = 'success'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
891
+ login_header( '', $message );
892
+ ?>
893
  </div>
894
  <?php
895
  /** This action is documented in wp-login.php */
896
+ do_action( 'login_footer' );
897
+ ?>
898
  <?php if ( $customize_login ) : ?>
899
+ <script type="text/javascript">setTimeout( function(){ new wp.customize.Messenger({ url: '<?php echo esc_url( wp_customize_url() ); ?>', channel: 'login' }).send('login') }, 1000 );</script>
900
  <?php endif; ?>
901
  </body></html>
902
  <?php
915
  * @return array Updated array of columns.
916
  */
917
  public static function filter_manage_users_columns( array $columns ) {
918
+ $columns['two-factor'] = __( 'Two-Factor', 'two-factor' );
919
  return $columns;
920
  }
921
 
952
  * @param WP_User $user WP_User object of the logged-in user.
953
  */
954
  public static function user_two_factor_options( $user ) {
955
+ wp_enqueue_style( 'user-edit-2fa', plugins_url( 'user-edit.css', __FILE__ ), array(), TWO_FACTOR_VERSION );
956
 
957
  $enabled_providers = array_keys( self::get_available_providers_for_user( $user ) );
958
+ $primary_provider = self::get_primary_provider_for_user( $user->ID );
959
 
960
  if ( ! empty( $primary_provider ) && is_object( $primary_provider ) ) {
961
  $primary_provider_key = get_class( $primary_provider );
967
 
968
  ?>
969
  <input type="hidden" name="<?php echo esc_attr( self::ENABLED_PROVIDERS_USER_META_KEY ); ?>[]" value="<?php /* Dummy input so $_POST value is passed when no providers are enabled. */ ?>" />
970
+ <table class="form-table" id="two-factor-options">
971
  <tr>
972
  <th>
973
+ <?php esc_html_e( 'Two-Factor Options', 'two-factor' ); ?>
974
  </th>
975
  <td>
976
  <table class="two-factor-methods-table">
978
  <tr>
979
  <th class="col-enabled" scope="col"><?php esc_html_e( 'Enabled', 'two-factor' ); ?></th>
980
  <th class="col-primary" scope="col"><?php esc_html_e( 'Primary', 'two-factor' ); ?></th>
981
+ <th class="col-name" scope="col"><?php esc_html_e( 'Type', 'two-factor' ); ?></th>
982
  </tr>
983
  </thead>
984
  <tbody>
985
  <?php foreach ( self::get_providers() as $class => $object ) : ?>
986
  <tr>
987
+ <th scope="row"><input id="enabled-<?php echo esc_attr( $class ); ?>" type="checkbox" name="<?php echo esc_attr( self::ENABLED_PROVIDERS_USER_META_KEY ); ?>[]" value="<?php echo esc_attr( $class ); ?>" <?php checked( in_array( $class, $enabled_providers, true ) ); ?> /></th>
988
  <th scope="row"><input type="radio" name="<?php echo esc_attr( self::PROVIDER_USER_META_KEY ); ?>" value="<?php echo esc_attr( $class ); ?>" <?php checked( $class, $primary_provider_key ); ?> /></th>
989
  <td>
990
+ <label class="two-factor-method-label" for="enabled-<?php echo esc_attr( $class ); ?>"><?php echo esc_html( $object->get_label() ); ?></label>
991
+ <?php
992
+ /**
993
+ * Fires after user options are shown.
994
+ *
995
+ * Use the {@see 'two_factor_user_options_' . $class } hook instead.
996
+ *
997
+ * @deprecated 0.7.0
998
+ *
999
+ * @param WP_User $user The user.
1000
+ */
1001
+ do_action_deprecated( 'two-factor-user-options-' . $class, array( $user ), '0.7.0', 'two_factor_user_options_' . $class );
1002
+ do_action( 'two_factor_user_options_' . $class, $user );
1003
+ ?>
1004
  </td>
1005
  </tr>
1006
  <?php endforeach; ?>
1053
  }
1054
  }
1055
  }
1056
+
1057
+ /**
1058
+ * Should the login session persist between sessions.
1059
+ *
1060
+ * @return boolean
1061
+ */
1062
+ public static function rememberme() {
1063
+ $rememberme = false;
1064
+
1065
+ if ( ! empty( $_REQUEST['rememberme'] ) ) {
1066
+ $rememberme = true;
1067
+ }
1068
+
1069
+ return (bool) apply_filters( 'two_factor_rememberme', $rememberme );
1070
+ }
1071
  }
includes/Google/u2f-api.js CHANGED
@@ -16,12 +16,6 @@
16
  */
17
  var u2f = u2f || {};
18
 
19
- /**
20
- * Check if browser supports U2F API before this wrapper was added.
21
- * @type {int}
22
- */
23
- u2f.HasNativeApiSupport = ( ( u2f && u2f.register ) || ( chrome && chrome.runtime ) );
24
-
25
  /**
26
  * FIDO U2F Javascript API Version
27
  * @number
@@ -43,7 +37,7 @@ var js_api_version;
43
 
44
 
45
  /**
46
- * Message types for messsages to/from the extension
47
  * @const
48
  * @enum {string}
49
  */
@@ -294,7 +288,7 @@ u2f.WrappedChromeRuntimePort_ = function(port) {
294
  u2f.formatSignRequest_ =
295
  function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
296
  if (js_api_version === undefined || js_api_version < 1.1) {
297
- // Adapt request to the 1.0 JS API
298
  var signRequests = [];
299
  for (var i = 0; i < registeredKeys.length; i++) {
300
  signRequests[i] = {
@@ -311,7 +305,7 @@ u2f.formatSignRequest_ =
311
  requestId: reqId
312
  };
313
  }
314
- // JS 1.1 API
315
  return {
316
  type: u2f.MessageTypes.U2F_SIGN_REQUEST,
317
  appId: appId,
@@ -333,7 +327,7 @@ u2f.formatSignRequest_ =
333
  u2f.formatRegisterRequest_ =
334
  function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
335
  if (js_api_version === undefined || js_api_version < 1.1) {
336
- // Adapt request to the 1.0 JS API
337
  for (var i = 0; i < registerRequests.length; i++) {
338
  registerRequests[i].appId = appId;
339
  }
@@ -354,7 +348,7 @@ u2f.formatRegisterRequest_ =
354
  requestId: reqId
355
  };
356
  }
357
- // JS 1.1 API
358
  return {
359
  type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
360
  appId: appId,
@@ -386,7 +380,7 @@ u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
386
  var name = eventName.toLowerCase();
387
  if (name == 'message' || name == 'onmessage') {
388
  this.port_.onMessage.addListener(function(message) {
389
- // Emulate a minimal MessageEvent object
390
  handler({'data': message});
391
  });
392
  } else {
16
  */
17
  var u2f = u2f || {};
18
 
 
 
 
 
 
 
19
  /**
20
  * FIDO U2F Javascript API Version
21
  * @number
37
 
38
 
39
  /**
40
+ * Message types for messages to/from the extension
41
  * @const
42
  * @enum {string}
43
  */
288
  u2f.formatSignRequest_ =
289
  function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
290
  if (js_api_version === undefined || js_api_version < 1.1) {
291
+ // Adapt request to the 1.0 JS API.
292
  var signRequests = [];
293
  for (var i = 0; i < registeredKeys.length; i++) {
294
  signRequests[i] = {
305
  requestId: reqId
306
  };
307
  }
308
+ // JS 1.1 API.
309
  return {
310
  type: u2f.MessageTypes.U2F_SIGN_REQUEST,
311
  appId: appId,
327
  u2f.formatRegisterRequest_ =
328
  function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
329
  if (js_api_version === undefined || js_api_version < 1.1) {
330
+ // Adapt request to the 1.0 JS API.
331
  for (var i = 0; i < registerRequests.length; i++) {
332
  registerRequests[i].appId = appId;
333
  }
348
  requestId: reqId
349
  };
350
  }
351
+ // JS 1.1 API.
352
  return {
353
  type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
354
  appId: appId,
380
  var name = eventName.toLowerCase();
381
  if (name == 'message' || name == 'onmessage') {
382
  this.port_.onMessage.addListener(function(message) {
383
+ // Emulate a minimal MessageEvent object.
384
  handler({'data': message});
385
  });
386
  } else {
includes/Yubico/U2F.php CHANGED
@@ -164,7 +164,7 @@ class U2F
164
  $offs = 1;
165
  $pubKey = substr($rawReg, $offs, PUBKEY_LEN);
166
  $offs += PUBKEY_LEN;
167
- // decode the pubKey to make sure it's good
168
  $tmpKey = $this->pubkey_to_pem($pubKey);
169
  if($tmpKey === null) {
170
  throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
@@ -175,7 +175,7 @@ class U2F
175
  $offs += $khLen;
176
  $registration->keyHandle = $this->base64u_encode($kh);
177
 
178
- // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes)
179
  $certLen = 4;
180
  $certLen += ($regData[$offs + 2] << 8);
181
  $certLen += $regData[$offs + 3];
164
  $offs = 1;
165
  $pubKey = substr($rawReg, $offs, PUBKEY_LEN);
166
  $offs += PUBKEY_LEN;
167
+ // Decode the pubKey to make sure it's good.
168
  $tmpKey = $this->pubkey_to_pem($pubKey);
169
  if($tmpKey === null) {
170
  throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
175
  $offs += $khLen;
176
  $registration->keyHandle = $this->base64u_encode($kh);
177
 
178
+ // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes).
179
  $certLen = 4;
180
  $certLen += ($regData[$offs + 2] << 8);
181
  $certLen += $regData[$offs + 3];
includes/function.login-footer.php ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Extracted from wp-login.php since that file also loads WP core which we already have.
4
+ */
5
+
6
+ /**
7
+ * Outputs the footer for the login page.
8
+ *
9
+ * @since 3.1.0
10
+ *
11
+ * @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
12
+ * upon successful login.
13
+ *
14
+ * @param string $input_id Which input to auto-focus.
15
+ */
16
+ function login_footer( $input_id = '' ) {
17
+ global $interim_login;
18
+
19
+ // Don't allow interim logins to navigate away from the page.
20
+ if ( ! $interim_login ) {
21
+ ?>
22
+ <p id="backtoblog">
23
+ <?php
24
+ $html_link = sprintf(
25
+ '<a href="%s">%s</a>',
26
+ esc_url( home_url( '/' ) ),
27
+ sprintf(
28
+ /* translators: %s: Site title. */
29
+ _x( '&larr; Go to %s', 'site' ),
30
+ get_bloginfo( 'title', 'display' )
31
+ )
32
+ );
33
+ /**
34
+ * Filter the "Go to site" link displayed in the login page footer.
35
+ *
36
+ * @since 5.7.0
37
+ *
38
+ * @param string $link HTML link to the home URL of the current site.
39
+ */
40
+ echo apply_filters( 'login_site_html_link', $html_link );
41
+ ?>
42
+ </p>
43
+ <?php
44
+
45
+ the_privacy_policy_link( '<div class="privacy-policy-page-link">', '</div>' );
46
+ }
47
+
48
+ ?>
49
+ </div><?php // End of <div id="login">. ?>
50
+
51
+ <?php
52
+
53
+ if ( ! empty( $input_id ) ) {
54
+ ?>
55
+ <script type="text/javascript">
56
+ try{document.getElementById('<?php echo $input_id; ?>').focus();}catch(e){}
57
+ if(typeof wpOnload==='function')wpOnload();
58
+ </script>
59
+ <?php
60
+ }
61
+
62
+ /**
63
+ * Fires in the login page footer.
64
+ *
65
+ * @since 3.1.0
66
+ */
67
+ do_action( 'login_footer' );
68
+
69
+ ?>
70
+ <div class="clear"></div>
71
+ </body>
72
+ </html>
73
+ <?php
74
+ }
75
+
76
+ /**
77
+ * Outputs the JavaScript to handle the form shaking on the login page.
78
+ *
79
+ * @since 3.0.0
80
+ */
81
+ function wp_shake_js() {
82
+ ?>
83
+ <script type="text/javascript">
84
+ document.querySelector('form').classList.add('shake');
85
+ </script>
86
+ <?php
87
+ }
includes/function.login-header.php CHANGED
@@ -6,6 +6,14 @@
6
  /**
7
  * Output the login page header.
8
  *
 
 
 
 
 
 
 
 
9
  * @param string $title Optional. WordPress login Page title to display in the `<title>` element.
10
  * Default 'Log In'.
11
  * @param string $message Optional. Message to display in header. Default empty.
@@ -14,8 +22,9 @@
14
  function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
15
  global $error, $interim_login, $action;
16
 
17
- // Don't index any of these forms
18
- add_action( 'login_head', 'wp_no_robots' );
 
19
 
20
  add_action( 'login_head', 'wp_login_viewport_meta' );
21
 
@@ -24,7 +33,7 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
24
  }
25
 
26
  // Shake it!
27
- $shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password' );
28
  /**
29
  * Filters the error codes array for shaking the login form.
30
  *
@@ -34,14 +43,20 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
34
  */
35
  $shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes );
36
 
37
- if ( $shake_error_codes && $wp_error->get_error_code() && in_array( $wp_error->get_error_code(), $shake_error_codes ) )
38
- add_action( 'login_head', 'wp_shake_js', 12 );
 
39
 
40
  $login_title = get_bloginfo( 'name', 'display' );
41
 
42
- /* translators: Login screen title. 1: Login screen name, 2: Network or site name */
43
  $login_title = sprintf( __( '%1$s &lsaquo; %2$s &#8212; WordPress' ), $title, $login_title );
44
 
 
 
 
 
 
45
  /**
46
  * Filters the title tag content for login page.
47
  *
@@ -53,14 +68,9 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
53
  $login_title = apply_filters( 'login_title', $login_title, $title );
54
 
55
  ?><!DOCTYPE html>
56
- <!--[if IE 8]>
57
- <html xmlns="http://www.w3.org/1999/xhtml" class="ie8" <?php language_attributes(); ?>>
58
- <![endif]-->
59
- <!--[if !(IE 8) ]><!-->
60
- <html xmlns="http://www.w3.org/1999/xhtml" <?php language_attributes(); ?>>
61
- <!--<![endif]-->
62
  <head>
63
- <meta http-equiv="Content-Type" content="<?php bloginfo('html_type'); ?>; charset=<?php bloginfo('charset'); ?>" />
64
  <title><?php echo $login_title; ?></title>
65
  <?php
66
 
@@ -69,9 +79,9 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
69
  /*
70
  * Remove all stored post data on logging out.
71
  * This could be added by add_action('login_head'...) like wp_shake_js(),
72
- * but maybe better if it's not removable by plugins
73
  */
74
- if ( 'loggedout' == $wp_error->get_error_code() ) {
75
  ?>
76
  <script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script>
77
  <?php
@@ -91,13 +101,7 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
91
  */
92
  do_action( 'login_head' );
93
 
94
- if ( is_multisite() ) {
95
- $login_header_url = network_home_url();
96
- $login_header_title = get_network()->site_name;
97
- } else {
98
- $login_header_url = __( 'https://wordpress.org/' );
99
- $login_header_title = __( 'Powered by WordPress' );
100
- }
101
 
102
  /**
103
  * Filters link URL of the header logo above login form.
@@ -108,38 +112,54 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
108
  */
109
  $login_header_url = apply_filters( 'login_headerurl', $login_header_url );
110
 
 
 
111
  /**
112
  * Filters the title attribute of the header logo above login form.
113
  *
114
  * @since 2.1.0
 
115
  *
116
  * @param string $login_header_title Login header logo title attribute.
117
  */
118
- $login_header_title = apply_filters( 'login_headertitle', $login_header_title );
 
 
 
 
 
 
119
 
120
- /*
121
- * To match the URL/title set above, Multisite sites have the blog name,
122
- * while single sites get the header title.
 
 
 
 
 
123
  */
124
- if ( is_multisite() ) {
125
- $login_header_text = get_bloginfo( 'name', 'display' );
126
- } else {
127
- $login_header_text = $login_header_title;
128
- }
129
 
130
  $classes = array( 'login-action-' . $action, 'wp-core-ui' );
131
- if ( is_rtl() )
 
132
  $classes[] = 'rtl';
 
 
133
  if ( $interim_login ) {
134
  $classes[] = 'interim-login';
 
135
  ?>
136
  <style type="text/css">html{background-color: transparent;}</style>
137
  <?php
138
 
139
- if ( 'success' === $interim_login )
140
  $classes[] = 'interim-login-success';
 
141
  }
142
- $classes[] =' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) );
 
143
 
144
  /**
145
  * Filters the login page body classes.
@@ -153,7 +173,10 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
153
 
154
  ?>
155
  </head>
156
- <body class="login <?php echo esc_attr( implode( ' ', $classes ) ); ?>">
 
 
 
157
  <?php
158
  /**
159
  * Fires in the login page header after the body tag is opened.
@@ -161,13 +184,11 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
161
  * @since 4.6.0
162
  */
163
  do_action( 'login_header' );
 
164
  ?>
165
  <div id="login">
166
- <h1><a href="<?php echo esc_url( $login_header_url ); ?>" title="<?php echo esc_attr( $login_header_title ); ?>" tabindex="-1"><?php echo $login_header_text; ?></a></h1>
167
  <?php
168
-
169
- unset( $login_header_url, $login_header_title );
170
-
171
  /**
172
  * Filters the message to display above the login form.
173
  *
@@ -176,27 +197,32 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
176
  * @param string $message Login message text.
177
  */
178
  $message = apply_filters( 'login_message', $message );
179
- if ( !empty( $message ) )
 
180
  echo $message . "\n";
 
181
 
182
- // In case a plugin uses $error rather than the $wp_errors object
183
- if ( !empty( $error ) ) {
184
- $wp_error->add('error', $error);
185
- unset($error);
186
  }
187
 
188
- if ( $wp_error->get_error_code() ) {
189
- $errors = '';
190
  $messages = '';
 
191
  foreach ( $wp_error->get_error_codes() as $code ) {
192
  $severity = $wp_error->get_error_data( $code );
193
  foreach ( $wp_error->get_error_messages( $code ) as $error_message ) {
194
- if ( 'message' == $severity )
195
  $messages .= ' ' . $error_message . "<br />\n";
196
- else
197
  $errors .= ' ' . $error_message . "<br />\n";
 
198
  }
199
  }
 
200
  if ( ! empty( $errors ) ) {
201
  /**
202
  * Filters the error messages displayed above the login form.
@@ -207,6 +233,7 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
207
  */
208
  echo '<div id="login_error">' . apply_filters( 'login_errors', $errors ) . "</div>\n";
209
  }
 
210
  if ( ! empty( $messages ) ) {
211
  /**
212
  * Filters instructional messages displayed above the login form.
@@ -218,8 +245,13 @@ function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
218
  echo '<p class="message">' . apply_filters( 'login_messages', $messages ) . "</p>\n";
219
  }
220
  }
221
- } // End of login_header()
222
 
 
 
 
 
 
223
  function wp_login_viewport_meta() {
224
  ?>
225
  <meta name="viewport" content="width=device-width" />
6
  /**
7
  * Output the login page header.
8
  *
9
+ * @since 2.1.0
10
+ *
11
+ * @global string $error Login error message set by deprecated pluggable wp_login() function
12
+ * or plugins replacing it.
13
+ * @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
14
+ * upon successful login.
15
+ * @global string $action The action that brought the visitor to the login page.
16
+ *
17
  * @param string $title Optional. WordPress login Page title to display in the `<title>` element.
18
  * Default 'Log In'.
19
  * @param string $message Optional. Message to display in header. Default empty.
22
  function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
23
  global $error, $interim_login, $action;
24
 
25
+ // Don't index any of these forms.
26
+ add_filter( 'wp_robots', 'wp_robots_sensitive_page' );
27
+ add_action( 'login_head', 'wp_strict_cross_origin_referrer' );
28
 
29
  add_action( 'login_head', 'wp_login_viewport_meta' );
30
 
33
  }
34
 
35
  // Shake it!
36
+ $shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password', 'retrieve_password_email_failure' );
37
  /**
38
  * Filters the error codes array for shaking the login form.
39
  *
43
  */
44
  $shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes );
45
 
46
+ if ( $shake_error_codes && $wp_error->has_errors() && in_array( $wp_error->get_error_code(), $shake_error_codes, true ) ) {
47
+ add_action( 'login_footer', 'wp_shake_js', 12 );
48
+ }
49
 
50
  $login_title = get_bloginfo( 'name', 'display' );
51
 
52
+ /* translators: Login screen title. 1: Login screen name, 2: Network or site name. */
53
  $login_title = sprintf( __( '%1$s &lsaquo; %2$s &#8212; WordPress' ), $title, $login_title );
54
 
55
+ if ( wp_is_recovery_mode() ) {
56
+ /* translators: %s: Login screen title. */
57
+ $login_title = sprintf( __( 'Recovery Mode &#8212; %s' ), $login_title );
58
+ }
59
+
60
  /**
61
  * Filters the title tag content for login page.
62
  *
68
  $login_title = apply_filters( 'login_title', $login_title, $title );
69
 
70
  ?><!DOCTYPE html>
71
+ <html <?php language_attributes(); ?>>
 
 
 
 
 
72
  <head>
73
+ <meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>" />
74
  <title><?php echo $login_title; ?></title>
75
  <?php
76
 
79
  /*
80
  * Remove all stored post data on logging out.
81
  * This could be added by add_action('login_head'...) like wp_shake_js(),
82
+ * but maybe better if it's not removable by plugins.
83
  */
84
+ if ( 'loggedout' === $wp_error->get_error_code() ) {
85
  ?>
86
  <script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script>
87
  <?php
101
  */
102
  do_action( 'login_head' );
103
 
104
+ $login_header_url = __( 'https://wordpress.org/' );
 
 
 
 
 
 
105
 
106
  /**
107
  * Filters link URL of the header logo above login form.
112
  */
113
  $login_header_url = apply_filters( 'login_headerurl', $login_header_url );
114
 
115
+ $login_header_title = '';
116
+
117
  /**
118
  * Filters the title attribute of the header logo above login form.
119
  *
120
  * @since 2.1.0
121
+ * @deprecated 5.2.0 Use {@see 'login_headertext'} instead.
122
  *
123
  * @param string $login_header_title Login header logo title attribute.
124
  */
125
+ $login_header_title = apply_filters_deprecated(
126
+ 'login_headertitle',
127
+ array( $login_header_title ),
128
+ '5.2.0',
129
+ 'login_headertext',
130
+ __( 'Usage of the title attribute on the login logo is not recommended for accessibility reasons. Use the link text instead.' )
131
+ );
132
 
133
+ $login_header_text = empty( $login_header_title ) ? __( 'Powered by WordPress' ) : $login_header_title;
134
+
135
+ /**
136
+ * Filters the link text of the header logo above the login form.
137
+ *
138
+ * @since 5.2.0
139
+ *
140
+ * @param string $login_header_text The login header logo link text.
141
  */
142
+ $login_header_text = apply_filters( 'login_headertext', $login_header_text );
 
 
 
 
143
 
144
  $classes = array( 'login-action-' . $action, 'wp-core-ui' );
145
+
146
+ if ( is_rtl() ) {
147
  $classes[] = 'rtl';
148
+ }
149
+
150
  if ( $interim_login ) {
151
  $classes[] = 'interim-login';
152
+
153
  ?>
154
  <style type="text/css">html{background-color: transparent;}</style>
155
  <?php
156
 
157
+ if ( 'success' === $interim_login ) {
158
  $classes[] = 'interim-login-success';
159
+ }
160
  }
161
+
162
+ $classes[] = ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) );
163
 
164
  /**
165
  * Filters the login page body classes.
173
 
174
  ?>
175
  </head>
176
+ <body class="login no-js <?php echo esc_attr( implode( ' ', $classes ) ); ?>">
177
+ <script type="text/javascript">
178
+ document.body.className = document.body.className.replace('no-js','js');
179
+ </script>
180
  <?php
181
  /**
182
  * Fires in the login page header after the body tag is opened.
184
  * @since 4.6.0
185
  */
186
  do_action( 'login_header' );
187
+
188
  ?>
189
  <div id="login">
190
+ <h1><a href="<?php echo esc_url( $login_header_url ); ?>"><?php echo $login_header_text; ?></a></h1>
191
  <?php
 
 
 
192
  /**
193
  * Filters the message to display above the login form.
194
  *
197
  * @param string $message Login message text.
198
  */
199
  $message = apply_filters( 'login_message', $message );
200
+
201
+ if ( ! empty( $message ) ) {
202
  echo $message . "\n";
203
+ }
204
 
205
+ // In case a plugin uses $error rather than the $wp_errors object.
206
+ if ( ! empty( $error ) ) {
207
+ $wp_error->add( 'error', $error );
208
+ unset( $error );
209
  }
210
 
211
+ if ( $wp_error->has_errors() ) {
212
+ $errors = '';
213
  $messages = '';
214
+
215
  foreach ( $wp_error->get_error_codes() as $code ) {
216
  $severity = $wp_error->get_error_data( $code );
217
  foreach ( $wp_error->get_error_messages( $code ) as $error_message ) {
218
+ if ( 'message' === $severity ) {
219
  $messages .= ' ' . $error_message . "<br />\n";
220
+ } else {
221
  $errors .= ' ' . $error_message . "<br />\n";
222
+ }
223
  }
224
  }
225
+
226
  if ( ! empty( $errors ) ) {
227
  /**
228
  * Filters the error messages displayed above the login form.
233
  */
234
  echo '<div id="login_error">' . apply_filters( 'login_errors', $errors ) . "</div>\n";
235
  }
236
+
237
  if ( ! empty( $messages ) ) {
238
  /**
239
  * Filters instructional messages displayed above the login form.
245
  echo '<p class="message">' . apply_filters( 'login_messages', $messages ) . "</p>\n";
246
  }
247
  }
248
+ } // End of login_header().
249
 
250
+ /**
251
+ * Outputs the viewport meta tag for the login page.
252
+ *
253
+ * @since 3.7.0
254
+ */
255
  function wp_login_viewport_meta() {
256
  ?>
257
  <meta name="viewport" content="width=device-width" />
providers/{class.two-factor-backup-codes.php → class-two-factor-backup-codes.php} RENAMED
@@ -1,4 +1,10 @@
1
  <?php
 
 
 
 
 
 
2
  /**
3
  * Class for creating a backup codes provider.
4
  *
@@ -27,11 +33,11 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
27
  *
28
  * @since 0.1-dev
29
  */
30
- static function get_instance() {
31
  static $instance;
32
  $class = __CLASS__;
33
  if ( ! is_a( $instance, $class ) ) {
34
- $instance = new $class;
35
  }
36
  return $instance;
37
  }
@@ -42,7 +48,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
42
  * @since 0.1-dev
43
  */
44
  protected function __construct() {
45
- add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_options' ) );
46
  add_action( 'admin_notices', array( $this, 'admin_notices' ) );
47
  add_action( 'wp_ajax_two_factor_backup_codes_generate', array( $this, 'ajax_generate_json' ) );
48
 
@@ -58,7 +64,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
58
  $user = wp_get_current_user();
59
 
60
  // Return if the provider is not enabled.
61
- if ( ! in_array( __CLASS__, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ) ) ) {
62
  return;
63
  }
64
 
@@ -71,9 +77,13 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
71
  <p>
72
  <span>
73
  <?php
74
- printf( // WPCS: XSS OK.
75
- __( 'Two-Factor: You are out of backup codes and need to <a href="%s">regenerate!</a>', 'two-factor' ),
76
- esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' )
 
 
 
 
77
  );
78
  ?>
79
  <span>
@@ -116,19 +126,23 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
116
  */
117
  public function user_options( $user ) {
118
  $ajax_nonce = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID );
119
- $count = self::codes_remaining_for_user( $user );
120
  ?>
121
  <p id="two-factor-backup-codes">
122
  <button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
123
  <?php esc_html_e( 'Generate Verification Codes', 'two-factor' ); ?>
124
  </button>
125
- <span class="two-factor-backup-codes-count"><?php
126
- echo esc_html( sprintf(
 
 
127
  /* translators: %s: count */
128
- _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ),
129
- $count
130
- ) );
131
- ?></span>
 
 
132
  </p>
133
  <div class="two-factor-backup-codes-wrapper" style="display:none;">
134
  <ol class="two-factor-backup-codes-unused-codes"></ol>
@@ -163,7 +177,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
163
  // Update counter.
164
  $( '.two-factor-backup-codes-count' ).html( response.data.i18n.count );
165
 
166
- // Build the download link
167
  var txt_data = 'data:application/text;charset=utf-8,' + '\n';
168
  txt_data += response.data.i18n.title.replace( /%s/g, document.domain ) + '\n\n';
169
 
@@ -186,11 +200,11 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
186
  * @since 0.1-dev
187
  *
188
  * @param WP_User $user WP_User object of the logged-in user.
189
- * @param array $args Optional arguments for assinging new codes.
190
  * @return array
191
  */
192
  public function generate_codes( $user, $args = '' ) {
193
- $codes = array();
194
  $codes_hashed = array();
195
 
196
  // Check for arguments.
@@ -206,9 +220,9 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
206
  }
207
 
208
  for ( $i = 0; $i < $num_codes; $i++ ) {
209
- $code = $this->get_code();
210
  $codes_hashed[] = wp_hash_password( $code );
211
- $codes[] = $code;
212
  unset( $code );
213
  }
214
 
@@ -224,13 +238,13 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
224
  * @since 0.1-dev
225
  */
226
  public function ajax_generate_json() {
227
- $user = get_user_by( 'id', sanitize_text_field( $_POST['user_id'] ) );
228
  check_ajax_referer( 'two-factor-backup-codes-generate-json-' . $user->ID, 'nonce' );
229
 
230
  // Setup the return data.
231
  $codes = $this->generate_codes( $user );
232
  $count = self::codes_remaining_for_user( $user );
233
- $i18n = array(
234
  /* translators: %s: count */
235
  'count' => esc_html( sprintf( _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ), $count ) ),
236
  /* translators: %s: the site's domain */
@@ -238,7 +252,12 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
238
  );
239
 
240
  // Send the response.
241
- wp_send_json_success( array( 'codes' => $codes, 'i18n' => $i18n ) );
 
 
 
 
 
242
  }
243
 
244
  /**
@@ -263,7 +282,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
263
  * @param WP_User $user WP_User object of the logged-in user.
264
  */
265
  public function authentication_page( $user ) {
266
- require_once( ABSPATH . '/wp-admin/includes/template.php' );
267
  ?>
268
  <p><?php esc_html_e( 'Enter a backup verification code.', 'two-factor' ); ?></p><br/>
269
  <p>
@@ -285,7 +304,8 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
285
  * @return boolean
286
  */
287
  public function validate_authentication( $user ) {
288
- return $this->validate_code( $user, $_POST['two-factor-backup-code'] );
 
289
  }
290
 
291
  /**
1
  <?php
2
+ /**
3
+ * Class for creating a backup codes provider.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
  /**
9
  * Class for creating a backup codes provider.
10
  *
33
  *
34
  * @since 0.1-dev
35
  */
36
+ public static function get_instance() {
37
  static $instance;
38
  $class = __CLASS__;
39
  if ( ! is_a( $instance, $class ) ) {
40
+ $instance = new $class();
41
  }
42
  return $instance;
43
  }
48
  * @since 0.1-dev
49
  */
50
  protected function __construct() {
51
+ add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
52
  add_action( 'admin_notices', array( $this, 'admin_notices' ) );
53
  add_action( 'wp_ajax_two_factor_backup_codes_generate', array( $this, 'ajax_generate_json' ) );
54
 
64
  $user = wp_get_current_user();
65
 
66
  // Return if the provider is not enabled.
67
+ if ( ! in_array( __CLASS__, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ), true ) ) {
68
  return;
69
  }
70
 
77
  <p>
78
  <span>
79
  <?php
80
+ echo wp_kses(
81
+ sprintf(
82
+ /* translators: %s: URL for code regeneration */
83
+ __( 'Two-Factor: You are out of backup codes and need to <a href="%s">regenerate!</a>', 'two-factor' ),
84
+ esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' )
85
+ ),
86
+ array( 'a' => array( 'href' => true ) )
87
  );
88
  ?>
89
  <span>
126
  */
127
  public function user_options( $user ) {
128
  $ajax_nonce = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID );
129
+ $count = self::codes_remaining_for_user( $user );
130
  ?>
131
  <p id="two-factor-backup-codes">
132
  <button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
133
  <?php esc_html_e( 'Generate Verification Codes', 'two-factor' ); ?>
134
  </button>
135
+ <span class="two-factor-backup-codes-count">
136
+ <?php
137
+ echo esc_html(
138
+ sprintf(
139
  /* translators: %s: count */
140
+ _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ),
141
+ $count
142
+ )
143
+ );
144
+ ?>
145
+ </span>
146
  </p>
147
  <div class="two-factor-backup-codes-wrapper" style="display:none;">
148
  <ol class="two-factor-backup-codes-unused-codes"></ol>
177
  // Update counter.
178
  $( '.two-factor-backup-codes-count' ).html( response.data.i18n.count );
179
 
180
+ // Build the download link.
181
  var txt_data = 'data:application/text;charset=utf-8,' + '\n';
182
  txt_data += response.data.i18n.title.replace( /%s/g, document.domain ) + '\n\n';
183
 
200
  * @since 0.1-dev
201
  *
202
  * @param WP_User $user WP_User object of the logged-in user.
203
+ * @param array $args Optional arguments for assigning new codes.
204
  * @return array
205
  */
206
  public function generate_codes( $user, $args = '' ) {
207
+ $codes = array();
208
  $codes_hashed = array();
209
 
210
  // Check for arguments.
220
  }
221
 
222
  for ( $i = 0; $i < $num_codes; $i++ ) {
223
+ $code = $this->get_code();
224
  $codes_hashed[] = wp_hash_password( $code );
225
+ $codes[] = $code;
226
  unset( $code );
227
  }
228
 
238
  * @since 0.1-dev
239
  */
240
  public function ajax_generate_json() {
241
+ $user = get_user_by( 'id', filter_input( INPUT_POST, 'user_id', FILTER_SANITIZE_NUMBER_INT ) );
242
  check_ajax_referer( 'two-factor-backup-codes-generate-json-' . $user->ID, 'nonce' );
243
 
244
  // Setup the return data.
245
  $codes = $this->generate_codes( $user );
246
  $count = self::codes_remaining_for_user( $user );
247
+ $i18n = array(
248
  /* translators: %s: count */
249
  'count' => esc_html( sprintf( _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ), $count ) ),
250
  /* translators: %s: the site's domain */
252
  );
253
 
254
  // Send the response.
255
+ wp_send_json_success(
256
+ array(
257
+ 'codes' => $codes,
258
+ 'i18n' => $i18n,
259
+ )
260
+ );
261
  }
262
 
263
  /**
282
  * @param WP_User $user WP_User object of the logged-in user.
283
  */
284
  public function authentication_page( $user ) {
285
+ require_once ABSPATH . '/wp-admin/includes/template.php';
286
  ?>
287
  <p><?php esc_html_e( 'Enter a backup verification code.', 'two-factor' ); ?></p><br/>
288
  <p>
304
  * @return boolean
305
  */
306
  public function validate_authentication( $user ) {
307
+ $backup_code = isset( $_POST['two-factor-backup-code'] ) ? sanitize_text_field( wp_unslash( $_POST['two-factor-backup-code'] ) ) : '';
308
+ return $this->validate_code( $user, $backup_code );
309
  }
310
 
311
  /**
providers/{class.two-factor-dummy.php → class-two-factor-dummy.php} RENAMED
@@ -1,4 +1,10 @@
1
  <?php
 
 
 
 
 
 
2
  /**
3
  * Class for creating a dummy provider.
4
  *
@@ -13,11 +19,11 @@ class Two_Factor_Dummy extends Two_Factor_Provider {
13
  *
14
  * @since 0.1-dev
15
  */
16
- static function get_instance() {
17
  static $instance;
18
  $class = __CLASS__;
19
  if ( ! is_a( $instance, $class ) ) {
20
- $instance = new $class;
21
  }
22
  return $instance;
23
  }
@@ -28,7 +34,7 @@ class Two_Factor_Dummy extends Two_Factor_Provider {
28
  * @since 0.1-dev
29
  */
30
  protected function __construct() {
31
- add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_options' ) );
32
  return parent::__construct();
33
  }
34
 
@@ -49,7 +55,7 @@ class Two_Factor_Dummy extends Two_Factor_Provider {
49
  * @param WP_User $user WP_User object of the logged-in user.
50
  */
51
  public function authentication_page( $user ) {
52
- require_once( ABSPATH . '/wp-admin/includes/template.php' );
53
  ?>
54
  <p><?php esc_html_e( 'Are you really you?', 'two-factor' ); ?></p>
55
  <?php
1
  <?php
2
+ /**
3
+ * Class for creating a dummy provider.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
  /**
9
  * Class for creating a dummy provider.
10
  *
19
  *
20
  * @since 0.1-dev
21
  */
22
+ public static function get_instance() {
23
  static $instance;
24
  $class = __CLASS__;
25
  if ( ! is_a( $instance, $class ) ) {
26
+ $instance = new $class();
27
  }
28
  return $instance;
29
  }
34
  * @since 0.1-dev
35
  */
36
  protected function __construct() {
37
+ add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
38
  return parent::__construct();
39
  }
40
 
55
  * @param WP_User $user WP_User object of the logged-in user.
56
  */
57
  public function authentication_page( $user ) {
58
+ require_once ABSPATH . '/wp-admin/includes/template.php';
59
  ?>
60
  <p><?php esc_html_e( 'Are you really you?', 'two-factor' ); ?></p>
61
  <?php
providers/{class.two-factor-email.php → class-two-factor-email.php} RENAMED
@@ -1,4 +1,10 @@
1
  <?php
 
 
 
 
 
 
2
  /**
3
  * Class for creating an email provider.
4
  *
@@ -11,10 +17,17 @@ class Two_Factor_Email extends Two_Factor_Provider {
11
  /**
12
  * The user meta token key.
13
  *
14
- * @type string
15
  */
16
  const TOKEN_META_KEY = '_two_factor_email_token';
17
 
 
 
 
 
 
 
 
18
  /**
19
  * Name of the input field used for code resend.
20
  *
@@ -27,11 +40,11 @@ class Two_Factor_Email extends Two_Factor_Provider {
27
  *
28
  * @since 0.1-dev
29
  */
30
- static function get_instance() {
31
  static $instance;
32
  $class = __CLASS__;
33
  if ( ! is_a( $instance, $class ) ) {
34
- $instance = new $class;
35
  }
36
  return $instance;
37
  }
@@ -42,7 +55,7 @@ class Two_Factor_Email extends Two_Factor_Provider {
42
  * @since 0.1-dev
43
  */
44
  protected function __construct() {
45
- add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_options' ) );
46
  return parent::__construct();
47
  }
48
 
@@ -65,7 +78,10 @@ class Two_Factor_Email extends Two_Factor_Provider {
65
  */
66
  public function generate_token( $user_id ) {
67
  $token = $this->get_code();
 
 
68
  update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
 
69
  return $token;
70
  }
71
 
@@ -80,9 +96,65 @@ class Two_Factor_Email extends Two_Factor_Provider {
80
 
81
  if ( ! empty( $hashed_token ) ) {
82
  return true;
83
- } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  return false;
85
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
 
88
  /**
@@ -115,11 +187,15 @@ class Two_Factor_Email extends Two_Factor_Provider {
115
  $hashed_token = $this->get_user_token( $user_id );
116
 
117
  // Bail if token is empty or it doesn't match.
118
- if ( empty( $hashed_token ) || ( wp_hash( $token ) !== $hashed_token ) ) {
119
  return false;
120
  }
121
 
122
- // Ensure that the token can't be re-used.
 
 
 
 
123
  $this->delete_token( $user_id );
124
 
125
  return true;
@@ -148,11 +224,28 @@ class Two_Factor_Email extends Two_Factor_Provider {
148
  $token = $this->generate_token( $user->ID );
149
 
150
  /* translators: %s: site name */
151
- $subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'two-factor' ), get_bloginfo( 'name' ) ) );
152
  /* translators: %s: token */
153
  $message = wp_strip_all_tags( sprintf( __( 'Enter %s to log in.', 'two-factor' ), $token ) );
154
 
155
- return wp_mail( $user->user_email, $subject, $message );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  }
157
 
158
  /**
@@ -167,16 +260,16 @@ class Two_Factor_Email extends Two_Factor_Provider {
167
  return;
168
  }
169
 
170
- if ( ! $this->user_has_token( $user->ID ) ) {
171
  $this->generate_and_email_token( $user );
172
  }
173
 
174
- require_once( ABSPATH . '/wp-admin/includes/template.php' );
175
  ?>
176
  <p><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p>
177
  <p>
178
  <label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
179
- <input type="tel" name="two-factor-email-code" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
180
  <?php submit_button( __( 'Log In', 'two-factor' ) ); ?>
181
  </p>
182
  <p class="two-factor-email-resend">
@@ -224,7 +317,10 @@ class Two_Factor_Email extends Two_Factor_Provider {
224
  return false;
225
  }
226
 
227
- return $this->validate_token( $user->ID, $_REQUEST['two-factor-email-code'] );
 
 
 
228
  }
229
 
230
  /**
@@ -251,11 +347,13 @@ class Two_Factor_Email extends Two_Factor_Provider {
251
  ?>
252
  <div>
253
  <?php
254
- echo esc_html( sprintf(
 
255
  /* translators: %s: email address */
256
- __( 'Authentication codes will be sent to %s.', 'two-factor' ),
257
- $email
258
- ) );
 
259
  ?>
260
  </div>
261
  <?php
1
  <?php
2
+ /**
3
+ * Class for creating an email provider.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
  /**
9
  * Class for creating an email provider.
10
  *
17
  /**
18
  * The user meta token key.
19
  *
20
+ * @var string
21
  */
22
  const TOKEN_META_KEY = '_two_factor_email_token';
23
 
24
+ /**
25
+ * Store the timestamp when the token was generated.
26
+ *
27
+ * @var string
28
+ */
29
+ const TOKEN_META_KEY_TIMESTAMP = '_two_factor_email_token_timestamp';
30
+
31
  /**
32
  * Name of the input field used for code resend.
33
  *
40
  *
41
  * @since 0.1-dev
42
  */
43
+ public static function get_instance() {
44
  static $instance;
45
  $class = __CLASS__;
46
  if ( ! is_a( $instance, $class ) ) {
47
+ $instance = new $class();
48
  }
49
  return $instance;
50
  }
55
  * @since 0.1-dev
56
  */
57
  protected function __construct() {
58
+ add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
59
  return parent::__construct();
60
  }
61
 
78
  */
79
  public function generate_token( $user_id ) {
80
  $token = $this->get_code();
81
+
82
+ update_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, time() );
83
  update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
84
+
85
  return $token;
86
  }
87
 
96
 
97
  if ( ! empty( $hashed_token ) ) {
98
  return true;
99
+ }
100
+
101
+ return false;
102
+ }
103
+
104
+ /**
105
+ * Has the user token validity timestamp expired.
106
+ *
107
+ * @param integer $user_id User ID.
108
+ *
109
+ * @return boolean
110
+ */
111
+ public function user_token_has_expired( $user_id ) {
112
+ $token_lifetime = $this->user_token_lifetime( $user_id );
113
+ $token_ttl = $this->user_token_ttl( $user_id );
114
+
115
+ // Invalid token lifetime is considered an expired token.
116
+ if ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl ) {
117
  return false;
118
  }
119
+
120
+ return true;
121
+ }
122
+
123
+ /**
124
+ * Get the lifetime of a user token in seconds.
125
+ *
126
+ * @param integer $user_id User ID.
127
+ *
128
+ * @return integer|null Return `null` if the lifetime can't be measured.
129
+ */
130
+ public function user_token_lifetime( $user_id ) {
131
+ $timestamp = intval( get_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, true ) );
132
+
133
+ if ( ! empty( $timestamp ) ) {
134
+ return time() - $timestamp;
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Return the token time-to-live for a user.
142
+ *
143
+ * @param integer $user_id User ID.
144
+ *
145
+ * @return integer
146
+ */
147
+ public function user_token_ttl( $user_id ) {
148
+ $token_ttl = 15 * MINUTE_IN_SECONDS;
149
+
150
+ /**
151
+ * Number of seconds the token is considered valid
152
+ * after the generation.
153
+ *
154
+ * @param integer $token_ttl Token time-to-live in seconds.
155
+ * @param integer $user_id User ID.
156
+ */
157
+ return (int) apply_filters( 'two_factor_token_ttl', $token_ttl, $user_id );
158
  }
159
 
160
  /**
187
  $hashed_token = $this->get_user_token( $user_id );
188
 
189
  // Bail if token is empty or it doesn't match.
190
+ if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) {
191
  return false;
192
  }
193
 
194
+ if ( $this->user_token_has_expired( $user_id ) ) {
195
+ return false;
196
+ }
197
+
198
+ // Ensure the token can be used only once.
199
  $this->delete_token( $user_id );
200
 
201
  return true;
224
  $token = $this->generate_token( $user->ID );
225
 
226
  /* translators: %s: site name */
227
+ $subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'two-factor' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) );
228
  /* translators: %s: token */
229
  $message = wp_strip_all_tags( sprintf( __( 'Enter %s to log in.', 'two-factor' ), $token ) );
230
 
231
+ /**
232
+ * Filter the token email subject.
233
+ *
234
+ * @param string $subject The email subject line.
235
+ * @param int $user_id The ID of the user.
236
+ */
237
+ $subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID );
238
+
239
+ /**
240
+ * Filter the token email message.
241
+ *
242
+ * @param string $message The email message.
243
+ * @param string $token The token.
244
+ * @param int $user_id The ID of the user.
245
+ */
246
+ $message = apply_filters( 'two_factor_token_email_message', $message, $token, $user->ID );
247
+
248
+ return wp_mail( $user->user_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
249
  }
250
 
251
  /**
260
  return;
261
  }
262
 
263
+ if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
264
  $this->generate_and_email_token( $user );
265
  }
266
 
267
+ require_once ABSPATH . '/wp-admin/includes/template.php';
268
  ?>
269
  <p><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p>
270
  <p>
271
  <label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
272
+ <input type="tel" name="two-factor-email-code" id="authcode" class="input" value="" size="20" />
273
  <?php submit_button( __( 'Log In', 'two-factor' ) ); ?>
274
  </p>
275
  <p class="two-factor-email-resend">
317
  return false;
318
  }
319
 
320
+ // Ensure there are no spaces or line breaks around the code.
321
+ $code = trim( sanitize_text_field( $_REQUEST['two-factor-email-code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already.
322
+
323
+ return $this->validate_token( $user->ID, $code );
324
  }
325
 
326
  /**
347
  ?>
348
  <div>
349
  <?php
350
+ echo esc_html(
351
+ sprintf(
352
  /* translators: %s: email address */
353
+ __( 'Authentication codes will be sent to %s.', 'two-factor' ),
354
+ $email
355
+ )
356
+ );
357
  ?>
358
  </div>
359
  <?php
providers/{class.two-factor-fido-u2f-admin-list-table.php → class-two-factor-fido-u2f-admin-list-table.php} RENAMED
@@ -1,4 +1,10 @@
1
  <?php
 
 
 
 
 
 
2
  // Load the parent class if it doesn't exist.
3
  if ( ! class_exists( 'WP_List_Table' ) ) {
4
  require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
@@ -24,7 +30,7 @@ class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
24
  public function get_columns() {
25
  return array(
26
  'name' => wp_strip_all_tags( __( 'Name', 'two-factor' ) ),
27
- 'added' => wp_strip_all_tags( __( 'Added', 'two-factor' ) ),
28
  'last_used' => wp_strip_all_tags( __( 'Last Used', 'two-factor' ) ),
29
  );
30
  }
@@ -35,10 +41,10 @@ class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
35
  * @since 0.1-dev
36
  */
37
  public function prepare_items() {
38
- $columns = $this->get_columns();
39
- $hidden = array();
40
- $sortable = array();
41
- $primary = 'name';
42
  $this->_column_headers = array( $columns, $hidden, $sortable, $primary );
43
  }
44
 
@@ -55,20 +61,20 @@ class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
55
  protected function column_default( $item, $column_name ) {
56
  switch ( $column_name ) {
57
  case 'name':
58
- $out = '<div class="hidden" id="inline_' . esc_attr( $item->keyHandle ) . '">';
59
  $out .= '<div class="name">' . esc_html( $item->name ) . '</div>';
60
  $out .= '</div>';
61
 
62
  $actions = array(
63
  'rename hide-if-no-js' => Two_Factor_FIDO_U2F_Admin::rename_link( $item ),
64
- 'delete' => Two_Factor_FIDO_U2F_Admin::delete_link( $item ),
65
  );
66
 
67
  return esc_html( $item->name ) . $out . self::row_actions( $actions );
68
  case 'added':
69
- return date( get_option( 'date_format', 'r' ), $item->added );
70
  case 'last_used':
71
- return date( get_option( 'date_format', 'r' ), $item->last_used );
72
  default:
73
  return 'WTF^^?';
74
  }
@@ -96,7 +102,7 @@ class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
96
  */
97
  public function single_row( $item ) {
98
  ?>
99
- <tr id="key-<?php echo esc_attr( $item->keyHandle ); ?>">
100
  <?php $this->single_row_columns( $item ); ?>
101
  </tr>
102
  <?php
@@ -115,8 +121,6 @@ class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
115
  <td colspan="<?php echo esc_attr( $this->get_column_count() ); ?>" class="colspanchange">
116
  <fieldset>
117
  <div class="inline-edit-col">
118
- <h4><?php esc_html_e( 'Quick Edit', 'two-factor' ); ?></h4>
119
-
120
  <label>
121
  <span class="title"><?php esc_html_e( 'Name', 'two-factor' ); ?></span>
122
  <span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" /></span>
@@ -124,7 +128,11 @@ class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
124
  </div>
125
  </fieldset>
126
  <?php
127
- $core_columns = array( 'name' => true, 'added' => true, 'last_used' => true );
 
 
 
 
128
  list( $columns ) = $this->get_column_info();
129
  foreach ( $columns as $column_name => $column_display_name ) {
130
  if ( isset( $core_columns[ $column_name ] ) ) {
1
  <?php
2
+ /**
3
+ * Class for displaying the list of security key items.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
  // Load the parent class if it doesn't exist.
9
  if ( ! class_exists( 'WP_List_Table' ) ) {
10
  require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
30
  public function get_columns() {
31
  return array(
32
  'name' => wp_strip_all_tags( __( 'Name', 'two-factor' ) ),
33
+ 'added' => wp_strip_all_tags( __( 'Added', 'two-factor' ) ),
34
  'last_used' => wp_strip_all_tags( __( 'Last Used', 'two-factor' ) ),
35
  );
36
  }
41
  * @since 0.1-dev
42
  */
43
  public function prepare_items() {
44
+ $columns = $this->get_columns();
45
+ $hidden = array();
46
+ $sortable = array();
47
+ $primary = 'name';
48
  $this->_column_headers = array( $columns, $hidden, $sortable, $primary );
49
  }
50
 
61
  protected function column_default( $item, $column_name ) {
62
  switch ( $column_name ) {
63
  case 'name':
64
+ $out = '<div class="hidden" id="inline_' . esc_attr( $item->keyHandle ) . '">'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
65
  $out .= '<div class="name">' . esc_html( $item->name ) . '</div>';
66
  $out .= '</div>';
67
 
68
  $actions = array(
69
  'rename hide-if-no-js' => Two_Factor_FIDO_U2F_Admin::rename_link( $item ),
70
+ 'delete' => Two_Factor_FIDO_U2F_Admin::delete_link( $item ),
71
  );
72
 
73
  return esc_html( $item->name ) . $out . self::row_actions( $actions );
74
  case 'added':
75
+ return gmdate( get_option( 'date_format', 'r' ), $item->added );
76
  case 'last_used':
77
+ return gmdate( get_option( 'date_format', 'r' ), $item->last_used );
78
  default:
79
  return 'WTF^^?';
80
  }
102
  */
103
  public function single_row( $item ) {
104
  ?>
105
+ <tr id="key-<?php echo esc_attr( $item->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ?>">
106
  <?php $this->single_row_columns( $item ); ?>
107
  </tr>
108
  <?php
121
  <td colspan="<?php echo esc_attr( $this->get_column_count() ); ?>" class="colspanchange">
122
  <fieldset>
123
  <div class="inline-edit-col">
 
 
124
  <label>
125
  <span class="title"><?php esc_html_e( 'Name', 'two-factor' ); ?></span>
126
  <span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" /></span>
128
  </div>
129
  </fieldset>
130
  <?php
131
+ $core_columns = array(
132
+ 'name' => true,
133
+ 'added' => true,
134
+ 'last_used' => true,
135
+ );
136
  list( $columns ) = $this->get_column_info();
137
  foreach ( $columns as $column_name => $column_display_name ) {
138
  if ( isset( $core_columns[ $column_name ] ) ) {
providers/{class.two-factor-fido-u2f-admin.php → class-two-factor-fido-u2f-admin.php} RENAMED
@@ -1,4 +1,10 @@
1
  <?php
 
 
 
 
 
 
2
  /**
3
  * Class for registering & modifying FIDO U2F security keys.
4
  *
@@ -24,13 +30,13 @@ class Two_Factor_FIDO_U2F_Admin {
24
  * @static
25
  */
26
  public static function add_hooks() {
27
- add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
28
  add_action( 'show_user_security_settings', array( __CLASS__, 'show_user_profile' ) );
29
- add_action( 'personal_options_update', array( __CLASS__, 'catch_submission' ), 0 );
30
- add_action( 'edit_user_profile_update', array( __CLASS__, 'catch_submission' ), 0 );
31
- add_action( 'load-profile.php', array( __CLASS__, 'catch_delete_security_key' ) );
32
- add_action( 'load-user-edit.php', array( __CLASS__, 'catch_delete_security_key' ) );
33
- add_action( 'wp_ajax_inline-save-key', array( __CLASS__, 'wp_ajax_inline_save' ) );
34
  }
35
 
36
  /**
@@ -44,16 +50,20 @@ class Two_Factor_FIDO_U2F_Admin {
44
  * @param string $hook Current page.
45
  */
46
  public static function enqueue_assets( $hook ) {
47
- if ( ! in_array( $hook, array( 'user-edit.php', 'profile.php' ) ) ) {
 
 
 
 
 
48
  return;
49
  }
50
 
51
- $user_id = get_current_user_id();
52
  $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id );
53
 
54
- // @todo Ensure that scripts don't fail because of missing u2fL10n
55
  try {
56
- $data = Two_Factor_FIDO_U2F::$u2f->getRegisterData( $security_keys );
57
  list( $req,$sigs ) = $data;
58
 
59
  update_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, $req );
@@ -65,14 +75,14 @@ class Two_Factor_FIDO_U2F_Admin {
65
  'fido-u2f-admin',
66
  plugins_url( 'css/fido-u2f-admin.css', __FILE__ ),
67
  null,
68
- '0.1.0-dev.1'
69
  );
70
 
71
  wp_enqueue_script(
72
  'fido-u2f-admin',
73
  plugins_url( 'js/fido-u2f-admin.js', __FILE__ ),
74
  array( 'jquery', 'fido-u2f-api' ),
75
- '0.1.0-dev.3',
76
  true
77
  );
78
 
@@ -81,14 +91,15 @@ class Two_Factor_FIDO_U2F_Admin {
81
  */
82
 
83
  $translation_array = array(
 
84
  'register' => array(
85
  'request' => $req,
86
- 'sigs' => $sigs,
87
  ),
88
- 'text' => array(
89
- 'insert' => esc_html__( 'Now insert (and tap) your Security Key.', 'two-factor' ),
90
- 'error' => esc_html__( 'U2F request failed.', 'two-factor' ),
91
- 'error_codes' => array(
92
  // Map u2f.ErrorCodes to error messages.
93
  0 => esc_html__( 'Request OK.', 'two-factor' ),
94
  1 => esc_html__( 'Other U2F error.', 'two-factor' ),
@@ -97,7 +108,7 @@ class Two_Factor_FIDO_U2F_Admin {
97
  4 => esc_html__( 'U2F device ineligible.', 'two-factor' ),
98
  5 => esc_html__( 'U2F request timeout reached.', 'two-factor' ),
99
  ),
100
- 'u2f_not_supported' => esc_html__( 'FIDO U2F is not supported in your web browser. Try using Google Chrome.', 'two-factor' ),
101
  ),
102
  );
103
 
@@ -115,7 +126,7 @@ class Two_Factor_FIDO_U2F_Admin {
115
  'inline-edit-key',
116
  plugins_url( 'js/fido-u2f-admin-inline-edit.js', __FILE__ ),
117
  array( 'jquery' ),
118
- '0.1.0-dev.1',
119
  true
120
  );
121
 
@@ -128,6 +139,18 @@ class Two_Factor_FIDO_U2F_Admin {
128
  );
129
  }
130
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  /**
132
  * Display the security key section in a users profile.
133
  *
@@ -185,8 +208,8 @@ class Two_Factor_FIDO_U2F_Admin {
185
  <p><a href="https://support.google.com/accounts/answer/6103523"><?php esc_html_e( 'You can find FIDO U2F Security Key devices for sale from here.', 'two-factor' ); ?></a></p>
186
 
187
  <?php
188
- require( TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f-admin-list-table.php' );
189
- $u2f_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table();
190
  $u2f_list_table->items = $security_keys;
191
  $u2f_list_table->prepare_items();
192
  $u2f_list_table->display();
@@ -215,7 +238,7 @@ class Two_Factor_FIDO_U2F_Admin {
215
 
216
  try {
217
  $response = json_decode( stripslashes( $_POST['u2f_response'] ) );
218
- $reg = Two_Factor_FIDO_U2F::$u2f->doRegister( get_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, true ), $response );
219
  $reg->new = true;
220
 
221
  Two_Factor_FIDO_U2F::add_security_key( $user_id, $reg );
@@ -225,9 +248,14 @@ class Two_Factor_FIDO_U2F_Admin {
225
 
226
  delete_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY );
227
 
228
- wp_safe_redirect( add_query_arg( array(
229
- 'new_app_pass' => 1,
230
- ), wp_get_referer() ) . '#security-keys-section' );
 
 
 
 
 
231
  exit;
232
  }
233
  }
@@ -243,14 +271,17 @@ class Two_Factor_FIDO_U2F_Admin {
243
  * @static
244
  */
245
  public static function catch_delete_security_key() {
246
- $user_id = get_current_user_id();
247
- if ( ! empty( $_REQUEST['delete_security_key'] ) ) {
 
248
  $slug = $_REQUEST['delete_security_key'];
 
249
  check_admin_referer( "delete_security_key-{$slug}", '_nonce_delete_security_key' );
250
 
251
  Two_Factor_FIDO_U2F::delete_security_key( $user_id, $slug );
252
 
253
  wp_safe_redirect( remove_query_arg( 'new_app_pass', wp_get_referer() ) . '#security-keys-section' );
 
254
  }
255
  }
256
 
@@ -281,7 +312,7 @@ class Two_Factor_FIDO_U2F_Admin {
281
  * @return string
282
  */
283
  public static function delete_link( $item ) {
284
- $delete_link = add_query_arg( 'delete_security_key', $item->keyHandle );
285
  $delete_link = wp_nonce_url( $delete_link, "delete_security_key-{$item->keyHandle}", '_nonce_delete_security_key' );
286
  return sprintf( '<a href="%1$s">%2$s</a>', esc_url( $delete_link ), esc_html__( 'Delete', 'two-factor' ) );
287
  }
@@ -297,22 +328,21 @@ class Two_Factor_FIDO_U2F_Admin {
297
  public static function wp_ajax_inline_save() {
298
  check_ajax_referer( 'keyinlineeditnonce', '_inline_edit' );
299
 
300
- require( TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f-admin-list-table.php' );
301
  $wp_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table();
302
 
303
  if ( ! isset( $_POST['keyHandle'] ) ) {
304
  wp_die();
305
  }
306
 
307
- $user_id = get_current_user_id();
308
-
309
  $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id );
310
  if ( ! $security_keys ) {
311
  wp_die();
312
  }
313
 
314
  foreach ( $security_keys as &$key ) {
315
- if ( $key->keyHandle === $_POST['keyHandle'] ) {
316
  break;
317
  }
318
  }
1
  <?php
2
+ /**
3
+ * Class for registering & modifying FIDO U2F security keys.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
  /**
9
  * Class for registering & modifying FIDO U2F security keys.
10
  *
30
  * @static
31
  */
32
  public static function add_hooks() {
33
+ add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
34
  add_action( 'show_user_security_settings', array( __CLASS__, 'show_user_profile' ) );
35
+ add_action( 'personal_options_update', array( __CLASS__, 'catch_submission' ), 0 );
36
+ add_action( 'edit_user_profile_update', array( __CLASS__, 'catch_submission' ), 0 );
37
+ add_action( 'load-profile.php', array( __CLASS__, 'catch_delete_security_key' ) );
38
+ add_action( 'load-user-edit.php', array( __CLASS__, 'catch_delete_security_key' ) );
39
+ add_action( 'wp_ajax_inline-save-key', array( __CLASS__, 'wp_ajax_inline_save' ) );
40
  }
41
 
42
  /**
50
  * @param string $hook Current page.
51
  */
52
  public static function enqueue_assets( $hook ) {
53
+ if ( ! in_array( $hook, array( 'user-edit.php', 'profile.php' ), true ) ) {
54
+ return;
55
+ }
56
+
57
+ $user_id = Two_Factor_Core::current_user_being_edited();
58
+ if ( ! $user_id ) {
59
  return;
60
  }
61
 
 
62
  $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id );
63
 
64
+ // @todo Ensure that scripts don't fail because of missing u2fL10n.
65
  try {
66
+ $data = Two_Factor_FIDO_U2F::$u2f->getRegisterData( $security_keys );
67
  list( $req,$sigs ) = $data;
68
 
69
  update_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, $req );
75
  'fido-u2f-admin',
76
  plugins_url( 'css/fido-u2f-admin.css', __FILE__ ),
77
  null,
78
+ self::asset_version()
79
  );
80
 
81
  wp_enqueue_script(
82
  'fido-u2f-admin',
83
  plugins_url( 'js/fido-u2f-admin.js', __FILE__ ),
84
  array( 'jquery', 'fido-u2f-api' ),
85
+ self::asset_version(),
86
  true
87
  );
88
 
91
  */
92
 
93
  $translation_array = array(
94
+ 'user_id' => $user_id,
95
  'register' => array(
96
  'request' => $req,
97
+ 'sigs' => $sigs,
98
  ),
99
+ 'text' => array(
100
+ 'insert' => esc_html__( 'Now insert (and tap) your Security Key.', 'two-factor' ),
101
+ 'error' => esc_html__( 'U2F request failed.', 'two-factor' ),
102
+ 'error_codes' => array(
103
  // Map u2f.ErrorCodes to error messages.
104
  0 => esc_html__( 'Request OK.', 'two-factor' ),
105
  1 => esc_html__( 'Other U2F error.', 'two-factor' ),
108
  4 => esc_html__( 'U2F device ineligible.', 'two-factor' ),
109
  5 => esc_html__( 'U2F request timeout reached.', 'two-factor' ),
110
  ),
111
+ 'u2f_not_supported' => esc_html__( 'FIDO U2F appears to be not supported by your web browser. Try using Google Chrome or Firefox.', 'two-factor' ),
112
  ),
113
  );
114
 
126
  'inline-edit-key',
127
  plugins_url( 'js/fido-u2f-admin-inline-edit.js', __FILE__ ),
128
  array( 'jquery' ),
129
+ self::asset_version(),
130
  true
131
  );
132
 
139
  );
140
  }
141
 
142
+ /**
143
+ * Return the current asset version number.
144
+ *
145
+ * Added as own helper to allow swapping the implementation once we inject
146
+ * it as a dependency.
147
+ *
148
+ * @return string
149
+ */
150
+ protected static function asset_version() {
151
+ return Two_Factor_FIDO_U2F::asset_version();
152
+ }
153
+
154
  /**
155
  * Display the security key section in a users profile.
156
  *
208
  <p><a href="https://support.google.com/accounts/answer/6103523"><?php esc_html_e( 'You can find FIDO U2F Security Key devices for sale from here.', 'two-factor' ); ?></a></p>
209
 
210
  <?php
211
+ require TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin-list-table.php';
212
+ $u2f_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table();
213
  $u2f_list_table->items = $security_keys;
214
  $u2f_list_table->prepare_items();
215
  $u2f_list_table->display();
238
 
239
  try {
240
  $response = json_decode( stripslashes( $_POST['u2f_response'] ) );
241
+ $reg = Two_Factor_FIDO_U2F::$u2f->doRegister( get_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, true ), $response );
242
  $reg->new = true;
243
 
244
  Two_Factor_FIDO_U2F::add_security_key( $user_id, $reg );
248
 
249
  delete_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY );
250
 
251
+ wp_safe_redirect(
252
+ add_query_arg(
253
+ array(
254
+ 'new_app_pass' => 1,
255
+ ),
256
+ wp_get_referer()
257
+ ) . '#security-keys-section'
258
+ );
259
  exit;
260
  }
261
  }
271
  * @static
272
  */
273
  public static function catch_delete_security_key() {
274
+ $user_id = Two_Factor_Core::current_user_being_edited();
275
+
276
+ if ( ! empty( $user_id ) && ! empty( $_REQUEST['delete_security_key'] ) ) {
277
  $slug = $_REQUEST['delete_security_key'];
278
+
279
  check_admin_referer( "delete_security_key-{$slug}", '_nonce_delete_security_key' );
280
 
281
  Two_Factor_FIDO_U2F::delete_security_key( $user_id, $slug );
282
 
283
  wp_safe_redirect( remove_query_arg( 'new_app_pass', wp_get_referer() ) . '#security-keys-section' );
284
+ exit;
285
  }
286
  }
287
 
312
  * @return string
313
  */
314
  public static function delete_link( $item ) {
315
+ $delete_link = add_query_arg( 'delete_security_key', $item->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
316
  $delete_link = wp_nonce_url( $delete_link, "delete_security_key-{$item->keyHandle}", '_nonce_delete_security_key' );
317
  return sprintf( '<a href="%1$s">%2$s</a>', esc_url( $delete_link ), esc_html__( 'Delete', 'two-factor' ) );
318
  }
328
  public static function wp_ajax_inline_save() {
329
  check_ajax_referer( 'keyinlineeditnonce', '_inline_edit' );
330
 
331
+ require TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin-list-table.php';
332
  $wp_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table();
333
 
334
  if ( ! isset( $_POST['keyHandle'] ) ) {
335
  wp_die();
336
  }
337
 
338
+ $user_id = Two_Factor_Core::current_user_being_edited();
 
339
  $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id );
340
  if ( ! $security_keys ) {
341
  wp_die();
342
  }
343
 
344
  foreach ( $security_keys as &$key ) {
345
+ if ( $key->keyHandle === $_POST['keyHandle'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
346
  break;
347
  }
348
  }
providers/{class.two-factor-fido-u2f.php → class-two-factor-fido-u2f.php} RENAMED
@@ -1,4 +1,10 @@
1
  <?php
 
 
 
 
 
 
2
  /**
3
  * Class for creating a FIDO Universal 2nd Factor provider.
4
  *
@@ -29,12 +35,19 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
29
  */
30
  const AUTH_DATA_USER_META_KEY = '_two_factor_fido_u2f_login_request';
31
 
 
 
 
 
 
 
 
32
  /**
33
  * Ensures only one instance of this class exists in memory at any one time.
34
  *
35
  * @return \Two_Factor_FIDO_U2F
36
  */
37
- static function get_instance() {
38
  static $instance;
39
 
40
  if ( ! isset( $instance ) ) {
@@ -54,33 +67,33 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
54
  return;
55
  }
56
 
57
- require_once( TWO_FACTOR_DIR . 'includes/Yubico/U2F.php' );
58
  self::$u2f = new u2flib_server\U2F( self::get_u2f_app_id() );
59
 
60
- require_once( TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f-admin.php' );
61
  Two_Factor_FIDO_U2F_Admin::add_hooks();
62
 
63
- wp_register_script(
64
- 'fido-u2f-api',
65
- plugins_url( 'includes/Google/u2f-api.js', dirname( __FILE__ ) ),
66
- null,
67
- '0.1.0-dev.2',
68
- true
69
- );
70
-
71
- wp_register_script(
72
- 'fido-u2f-login',
73
- plugins_url( 'js/fido-u2f-login.js', __FILE__ ),
74
- array( 'jquery', 'fido-u2f-api' ),
75
- '0.1.0-dev.2',
76
- true
77
- );
78
 
79
- add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_options' ) );
80
 
81
  return parent::__construct();
82
  }
83
 
 
 
 
 
 
 
 
 
 
 
 
84
  /**
85
  * Return the U2F AppId. U2F requires the AppID to use HTTPS
86
  * and a top-level domain.
@@ -103,16 +116,31 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
103
  * @since 0.1-dev
104
  */
105
  public function get_label() {
106
- return _x( 'FIDO Universal 2nd Factor (U2F)', 'Provider Label', 'two-factor' );
107
  }
108
 
109
  /**
110
- * Enqueue assets for login form.
 
111
  *
112
- * @since 0.1-dev
113
  */
114
- public function login_enqueue_assets() {
115
- wp_enqueue_script( 'fido-u2f-login' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  }
117
 
118
  /**
@@ -124,9 +152,9 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
124
  * @return null
125
  */
126
  public function authentication_page( $user ) {
127
- require_once( ABSPATH . '/wp-admin/includes/template.php' );
128
 
129
- // U2F doesn't work without HTTPS
130
  if ( ! is_ssl() ) {
131
  ?>
132
  <p><?php esc_html_e( 'U2F requires an HTTPS connection. Please use an alternative 2nd factor method.', 'two-factor' ); ?></p>
@@ -180,7 +208,7 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
180
  try {
181
  $reg = self::$u2f->doAuthenticate( $requests, $keys, $response );
182
 
183
- $reg->last_used = current_time( 'timestamp' );
184
 
185
  self::update_security_key( $user->ID, $reg );
186
 
@@ -233,8 +261,8 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
233
 
234
  if (
235
  ! is_object( $register )
236
- || ! property_exists( $register, 'keyHandle' ) || empty( $register->keyHandle )
237
- || ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey )
238
  || ! property_exists( $register, 'certificate' ) || empty( $register->certificate )
239
  || ! property_exists( $register, 'counter' ) || ( -1 > $register->counter )
240
  ) {
@@ -242,14 +270,14 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
242
  }
243
 
244
  $register = array(
245
- 'keyHandle' => $register->keyHandle,
246
- 'publicKey' => $register->publicKey,
247
  'certificate' => $register->certificate,
248
  'counter' => $register->counter,
249
  );
250
 
251
  $register['name'] = __( 'New Security Key', 'two-factor' );
252
- $register['added'] = current_time( 'timestamp' );
253
  $register['last_used'] = $register['added'];
254
 
255
  return add_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, $register );
@@ -300,8 +328,8 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
300
 
301
  if (
302
  ! is_object( $data )
303
- || ! property_exists( $data, 'keyHandle' ) || empty( $data->keyHandle )
304
- || ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey )
305
  || ! property_exists( $data, 'certificate' ) || empty( $data->certificate )
306
  || ! property_exists( $data, 'counter' ) || ( -1 > $data->counter )
307
  ) {
@@ -311,7 +339,7 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
311
  $keys = self::get_security_keys( $user_id );
312
  if ( $keys ) {
313
  foreach ( $keys as $key ) {
314
- if ( $key->keyHandle === $data->keyHandle ) {
315
  return update_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, (array) $data, (array) $key );
316
  }
317
  }
@@ -329,7 +357,7 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
329
  * @param string $keyHandle Optional. Key handle.
330
  * @return bool True on success, false on failure.
331
  */
332
- public static function delete_security_key( $user_id, $keyHandle = null ) {
333
  global $wpdb;
334
 
335
  if ( ! is_numeric( $user_id ) ) {
@@ -341,18 +369,21 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
341
  return false;
342
  }
343
 
344
- $table = $wpdb->usermeta;
 
345
 
346
- $keyHandle = wp_unslash( $keyHandle );
347
- $keyHandle = maybe_serialize( $keyHandle );
348
 
349
- $query = $wpdb->prepare( "SELECT umeta_id FROM $table WHERE meta_key = '%s' AND user_id = %d", self::REGISTERED_KEY_USER_META_KEY, $user_id );
 
350
 
351
- if ( $keyHandle ) {
352
- $query .= $wpdb->prepare( ' AND meta_value LIKE %s', '%:"' . $keyHandle . '";s:%' );
 
 
353
  }
354
 
355
- $meta_ids = $wpdb->get_col( $query );
356
  if ( ! count( $meta_ids ) ) {
357
  return false;
358
  }
1
  <?php
2
+ /**
3
+ * Class for creating a FIDO Universal 2nd Factor provider.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
  /**
9
  * Class for creating a FIDO Universal 2nd Factor provider.
10
  *
35
  */
36
  const AUTH_DATA_USER_META_KEY = '_two_factor_fido_u2f_login_request';
37
 
38
+ /**
39
+ * Version number for the bundled assets.
40
+ *
41
+ * @var string
42
+ */
43
+ const U2F_ASSET_VERSION = '0.2.1';
44
+
45
  /**
46
  * Ensures only one instance of this class exists in memory at any one time.
47
  *
48
  * @return \Two_Factor_FIDO_U2F
49
  */
50
+ public static function get_instance() {
51
  static $instance;
52
 
53
  if ( ! isset( $instance ) ) {
67
  return;
68
  }
69
 
70
+ require_once TWO_FACTOR_DIR . 'includes/Yubico/U2F.php';
71
  self::$u2f = new u2flib_server\U2F( self::get_u2f_app_id() );
72
 
73
+ require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin.php';
74
  Two_Factor_FIDO_U2F_Admin::add_hooks();
75
 
76
+ // Ensure the script dependencies have been registered before they're enqueued at a later priority.
77
+ add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
78
+ add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
79
+ add_action( 'login_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
82
 
83
  return parent::__construct();
84
  }
85
 
86
+ /**
87
+ * Get the asset version number.
88
+ *
89
+ * TODO: There should be a plugin-level helper for getting the current plugin version.
90
+ *
91
+ * @return string
92
+ */
93
+ public static function asset_version() {
94
+ return self::U2F_ASSET_VERSION;
95
+ }
96
+
97
  /**
98
  * Return the U2F AppId. U2F requires the AppID to use HTTPS
99
  * and a top-level domain.
116
  * @since 0.1-dev
117
  */
118
  public function get_label() {
119
+ return _x( 'FIDO U2F Security Keys', 'Provider Label', 'two-factor' );
120
  }
121
 
122
  /**
123
+ * Register script dependencies used during login and when
124
+ * registering keys in the WP admin.
125
  *
126
+ * @return void
127
  */
128
+ public static function enqueue_scripts() {
129
+ wp_register_script(
130
+ 'fido-u2f-api',
131
+ plugins_url( 'includes/Google/u2f-api.js', dirname( __FILE__ ) ),
132
+ null,
133
+ self::asset_version(),
134
+ true
135
+ );
136
+
137
+ wp_register_script(
138
+ 'fido-u2f-login',
139
+ plugins_url( 'js/fido-u2f-login.js', __FILE__ ),
140
+ array( 'jquery', 'fido-u2f-api' ),
141
+ self::asset_version(),
142
+ true
143
+ );
144
  }
145
 
146
  /**
152
  * @return null
153
  */
154
  public function authentication_page( $user ) {
155
+ require_once ABSPATH . '/wp-admin/includes/template.php';
156
 
157
+ // U2F doesn't work without HTTPS.
158
  if ( ! is_ssl() ) {
159
  ?>
160
  <p><?php esc_html_e( 'U2F requires an HTTPS connection. Please use an alternative 2nd factor method.', 'two-factor' ); ?></p>
208
  try {
209
  $reg = self::$u2f->doAuthenticate( $requests, $keys, $response );
210
 
211
+ $reg->last_used = time();
212
 
213
  self::update_security_key( $user->ID, $reg );
214
 
261
 
262
  if (
263
  ! is_object( $register )
264
+ || ! property_exists( $register, 'keyHandle' ) || empty( $register->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
265
+ || ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
266
  || ! property_exists( $register, 'certificate' ) || empty( $register->certificate )
267
  || ! property_exists( $register, 'counter' ) || ( -1 > $register->counter )
268
  ) {
270
  }
271
 
272
  $register = array(
273
+ 'keyHandle' => $register->keyHandle, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
274
+ 'publicKey' => $register->publicKey, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
275
  'certificate' => $register->certificate,
276
  'counter' => $register->counter,
277
  );
278
 
279
  $register['name'] = __( 'New Security Key', 'two-factor' );
280
+ $register['added'] = time();
281
  $register['last_used'] = $register['added'];
282
 
283
  return add_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, $register );
328
 
329
  if (
330
  ! is_object( $data )
331
+ || ! property_exists( $data, 'keyHandle' ) || empty( $data->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
332
+ || ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
333
  || ! property_exists( $data, 'certificate' ) || empty( $data->certificate )
334
  || ! property_exists( $data, 'counter' ) || ( -1 > $data->counter )
335
  ) {
339
  $keys = self::get_security_keys( $user_id );
340
  if ( $keys ) {
341
  foreach ( $keys as $key ) {
342
+ if ( $key->keyHandle === $data->keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
343
  return update_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, (array) $data, (array) $key );
344
  }
345
  }
357
  * @param string $keyHandle Optional. Key handle.
358
  * @return bool True on success, false on failure.
359
  */
360
+ public static function delete_security_key( $user_id, $keyHandle = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
361
  global $wpdb;
362
 
363
  if ( ! is_numeric( $user_id ) ) {
369
  return false;
370
  }
371
 
372
+ $keyHandle = wp_unslash( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
373
+ $keyHandle = maybe_serialize( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
374
 
375
+ $query = $wpdb->prepare( "SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id = %d", self::REGISTERED_KEY_USER_META_KEY, $user_id );
 
376
 
377
+ if ( $keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
378
+ $key_handle_lookup = sprintf( ':"%s";s:', $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
379
 
380
+ $query .= $wpdb->prepare(
381
+ ' AND meta_value LIKE %s',
382
+ '%' . $wpdb->esc_like( $key_handle_lookup ) . '%'
383
+ );
384
  }
385
 
386
+ $meta_ids = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
387
  if ( ! count( $meta_ids ) ) {
388
  return false;
389
  }
providers/{class.two-factor-provider.php → class-two-factor-provider.php} RENAMED
@@ -1,4 +1,10 @@
1
  <?php
 
 
 
 
 
 
2
  /**
3
  * Abstract class for creating two factor authentication providers.
4
  *
@@ -24,7 +30,7 @@ abstract class Two_Factor_Provider {
24
  *
25
  * @return string
26
  */
27
- abstract function get_label();
28
 
29
  /**
30
  * Prints the name of the provider.
@@ -42,7 +48,7 @@ abstract class Two_Factor_Provider {
42
  *
43
  * @param WP_User $user WP_User object of the logged-in user.
44
  */
45
- abstract function authentication_page( $user );
46
 
47
  /**
48
  * Allow providers to do extra processing before the authentication.
@@ -64,7 +70,7 @@ abstract class Two_Factor_Provider {
64
  * @param WP_User $user WP_User object of the logged-in user.
65
  * @return boolean
66
  */
67
- abstract function validate_authentication( $user );
68
 
69
  /**
70
  * Whether this Two Factor provider is configured and available for the user specified.
@@ -72,7 +78,7 @@ abstract class Two_Factor_Provider {
72
  * @param WP_User $user WP_User object of the logged-in user.
73
  * @return boolean
74
  */
75
- abstract function is_available_for_user( $user );
76
 
77
  /**
78
  * Generate a random eight-digit string to send out as an auth code.
1
  <?php
2
+ /**
3
+ * Abstract class for creating two factor authentication providers.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
  /**
9
  * Abstract class for creating two factor authentication providers.
10
  *
30
  *
31
  * @return string
32
  */
33
+ abstract public function get_label();
34
 
35
  /**
36
  * Prints the name of the provider.
48
  *
49
  * @param WP_User $user WP_User object of the logged-in user.
50
  */
51
+ abstract public function authentication_page( $user );
52
 
53
  /**
54
  * Allow providers to do extra processing before the authentication.
70
  * @param WP_User $user WP_User object of the logged-in user.
71
  * @return boolean
72
  */
73
+ abstract public function validate_authentication( $user );
74
 
75
  /**
76
  * Whether this Two Factor provider is configured and available for the user specified.
78
  * @param WP_User $user WP_User object of the logged-in user.
79
  * @return boolean
80
  */
81
+ abstract public function is_available_for_user( $user );
82
 
83
  /**
84
  * Generate a random eight-digit string to send out as an auth code.
providers/{class.two-factor-totp.php → class-two-factor-totp.php} RENAMED
@@ -24,25 +24,44 @@ class Two_Factor_Totp extends Two_Factor_Provider {
24
  */
25
  const NOTICES_META_KEY = '_two_factor_totp_notices';
26
 
27
- const DEFAULT_KEY_BIT_SIZE = 160;
28
- const DEFAULT_CRYPTO = 'sha1';
29
- const DEFAULT_DIGIT_COUNT = 6;
30
- const DEFAULT_TIME_STEP_SEC = 30;
 
 
 
 
 
 
 
31
  const DEFAULT_TIME_STEP_ALLOWANCE = 4;
32
- private static $_base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
 
 
 
 
 
 
33
 
34
  /**
35
  * Class constructor. Sets up hooks, etc.
 
 
36
  */
37
  protected function __construct() {
38
- add_action( 'two-factor-user-options-' . __CLASS__, array( $this, 'user_two_factor_options' ) );
39
- add_action( 'personal_options_update', array( $this, 'user_two_factor_options_update' ) );
40
- add_action( 'edit_user_profile_update', array( $this, 'user_two_factor_options_update' ) );
 
 
41
  return parent::__construct();
42
  }
43
 
44
  /**
45
  * Ensures only one instance of this class exists in memory at any one time.
 
 
46
  */
47
  public static function get_instance() {
48
  static $instance;
@@ -56,7 +75,36 @@ class Two_Factor_Totp extends Two_Factor_Provider {
56
  * Returns the name of the provider.
57
  */
58
  public function get_label() {
59
- return _x( 'Time Based One-Time Password (Google Authenticator)', 'Provider Label', 'two-factor' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
  /**
@@ -64,6 +112,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
64
  *
65
  * @param WP_User $user The current user being edited.
66
  * @return false
 
 
67
  */
68
  public function user_two_factor_options( $user ) {
69
  if ( ! isset( $user->ID ) ) {
@@ -73,19 +123,21 @@ class Two_Factor_Totp extends Two_Factor_Provider {
73
  wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false );
74
 
75
  $key = $this->get_user_totp_key( $user->ID );
76
- $this->admin_notices();
77
 
78
  ?>
79
  <div id="two-factor-totp-options">
80
- <?php if ( empty( $key ) ) :
81
- $key = $this->generate_key();
82
- $site_name = get_bloginfo( 'name', 'display' );
 
 
83
  ?>
84
  <p>
85
  <?php esc_html_e( 'Please scan the QR code or manually enter the key, then enter an authentication code from your app in order to complete setup.', 'two-factor' ); ?>
86
  </p>
87
  <p>
88
- <img src="<?php echo esc_url( $this->get_google_qr_code( $site_name . ':' . $user->user_login, $key, $site_name ) ); ?>" id="two-factor-totp-qrcode" />
89
  </p>
90
  <p>
91
  <code><?php echo esc_html( $key ); ?></code>
@@ -100,10 +152,10 @@ class Two_Factor_Totp extends Two_Factor_Provider {
100
  </p>
101
  <?php else : ?>
102
  <p class="success">
103
- <?php esc_html_e( 'Secret key configured and registered.', 'two-factor' ); ?>
104
  </p>
105
  <p>
106
- <input type="submit" class="button" name="two-factor-totp-delete" value="<?php esc_attr_e( 'Reset Key', 'two-factor' ); ?>" />
107
  <em class="description">
108
  <?php esc_html_e( 'You will have to re-scan the QR code on all devices as the previous codes will stop working.', 'two-factor' ); ?>
109
  </em>
@@ -117,27 +169,27 @@ class Two_Factor_Totp extends Two_Factor_Provider {
117
  * Save the options specified in `::user_two_factor_options()`
118
  *
119
  * @param integer $user_id The user ID whose options are being updated.
120
- * @return false
 
 
 
121
  */
122
  public function user_two_factor_options_update( $user_id ) {
123
  $notices = array();
124
- $errors = array();
125
-
126
- $current_key = $this->get_user_totp_key( $user_id );
127
 
128
  if ( isset( $_POST['_nonce_user_two_factor_totp_options'] ) ) {
129
  check_admin_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' );
130
 
131
- // Delete the secret key.
132
- if ( ! empty( $current_key ) && isset( $_POST['two-factor-totp-delete'] ) ) {
133
- $this->delete_user_totp_key( $user_id );
134
- }
135
-
136
  // Validate and store a new secret key.
137
  if ( ! empty( $_POST['two-factor-totp-authcode'] ) && ! empty( $_POST['two-factor-totp-key'] ) ) {
138
- if ( $this->is_valid_key( $_POST['two-factor-totp-key'] ) ) {
139
- if ( $this->is_valid_authcode( $_POST['two-factor-totp-key'], $_POST['two-factor-totp-authcode'] ) ) {
140
- if ( ! $this->set_user_totp_key( $user_id, $_POST['two-factor-totp-key'] ) ) {
 
 
 
 
141
  $errors[] = __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' );
142
  }
143
  } else {
@@ -200,7 +252,7 @@ class Two_Factor_Totp extends Two_Factor_Provider {
200
  * @return boolean
201
  */
202
  public function is_valid_key( $key ) {
203
- $check = sprintf( '/^[%s]+$/', self::$_base_32_chars );
204
 
205
  if ( 1 === preg_match( $check, $key ) ) {
206
  return true;
@@ -211,15 +263,22 @@ class Two_Factor_Totp extends Two_Factor_Provider {
211
 
212
  /**
213
  * Display any available admin notices.
 
 
 
 
 
 
214
  */
215
- public function admin_notices() {
216
- $notices = get_user_meta( get_current_user_id(), self::NOTICES_META_KEY, true );
217
 
218
  if ( ! empty( $notices ) ) {
219
- delete_user_meta( get_current_user_id(), self::NOTICES_META_KEY );
 
220
  foreach ( $notices as $class => $messages ) {
221
  ?>
222
- <div class="<?php echo esc_attr( $class ) ?>">
223
  <?php
224
  foreach ( $messages as $msg ) {
225
  ?>
@@ -241,12 +300,14 @@ class Two_Factor_Totp extends Two_Factor_Provider {
241
  * @param WP_User $user WP_User object of the logged-in user.
242
  *
243
  * @return bool Whether the user gave a valid code
 
 
244
  */
245
  public function validate_authentication( $user ) {
246
- if ( ! empty( $_REQUEST['authcode'] ) ) { // WPCS: CSRF ok, nonce verified by login_form_validate_2fa().
247
  return $this->is_valid_authcode(
248
  $this->get_user_totp_key( $user->ID ),
249
- sanitize_text_field( $_REQUEST['authcode'] ) // WPCS: CSRF ok, nonce verified by login_form_validate_2fa().
250
  );
251
  }
252
 
@@ -268,9 +329,12 @@ class Two_Factor_Totp extends Two_Factor_Provider {
268
  * Ticks are the allowed offset from the correct time in 30 second increments,
269
  * so the default of 4 allows codes that are two minutes to either side of server time
270
  *
 
271
  * @param int $max_ticks Max ticks of time correction to allow. Default 4.
272
  */
273
- $max_ticks = apply_filters( 'two-factor-totp-time-step-allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
 
 
274
 
275
  // Array of all ticks to allow, sorted using absolute value to test closest match first.
276
  $ticks = range( - $max_ticks, $max_ticks );
@@ -280,7 +344,7 @@ class Two_Factor_Totp extends Two_Factor_Provider {
280
 
281
  foreach ( $ticks as $offset ) {
282
  $log_time = $time + $offset;
283
- if ( self::calc_totp( $key, $log_time ) === $authcode ) {
284
  return true;
285
  }
286
  }
@@ -295,7 +359,7 @@ class Two_Factor_Totp extends Two_Factor_Provider {
295
  * @return string $bitsize long string composed of available base32 chars.
296
  */
297
  public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
298
- $bytes = ceil( $bitsize / 8 );
299
  $secret = wp_generate_password( $bytes, true, true );
300
 
301
  return self::base32_encode( $secret );
@@ -313,7 +377,7 @@ class Two_Factor_Totp extends Two_Factor_Provider {
313
  if ( PHP_INT_SIZE >= 8 ) {
314
  // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
315
  if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
316
- return pack( 'J', $value );
317
  }
318
  $highmap = 0xffffffff << 32;
319
  $higher = ( $value & $highmap ) >> 32;
@@ -325,8 +389,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
325
  $higher = 0;
326
  }
327
 
328
- $lowmap = 0xffffffff;
329
- $lower = $value & $lowmap;
330
 
331
  return pack( 'NN', $higher, $lower );
332
  }
@@ -373,10 +437,12 @@ class Two_Factor_Totp extends Two_Factor_Provider {
373
  * @param string $title The title to display in the Authentication app.
374
  *
375
  * @return string A URL to use as an img src to display the QR code
 
 
376
  */
377
  public static function get_google_qr_code( $name, $key, $title = null ) {
378
  // Encode to support spaces, question marks and other characters.
379
- $name = rawurlencode( $name );
380
  $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key );
381
  if ( isset( $title ) ) {
382
  $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) );
@@ -402,20 +468,24 @@ class Two_Factor_Totp extends Two_Factor_Provider {
402
  * Prints the form that prompts the user to authenticate.
403
  *
404
  * @param WP_User $user WP_User object of the logged-in user.
 
 
405
  */
406
  public function authentication_page( $user ) {
407
- require_once( ABSPATH . '/wp-admin/includes/template.php' );
408
  ?>
 
 
 
409
  <p>
410
  <label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
411
- <input type="tel" name="authcode" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
412
  </p>
413
  <script type="text/javascript">
414
  setTimeout( function(){
415
  var d;
416
  try{
417
  d = document.getElementById('authcode');
418
- d.value = '';
419
  d.focus();
420
  } catch(e){}
421
  }, 200);
@@ -443,10 +513,10 @@ class Two_Factor_Totp extends Two_Factor_Provider {
443
  }
444
 
445
  $five_bit_sections = str_split( $binary_string, 5 );
446
- $base32_string = '';
447
 
448
  foreach ( $five_bit_sections as $five_bit_section ) {
449
- $base32_string .= self::$_base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
450
  }
451
 
452
  return $base32_string;
@@ -463,25 +533,25 @@ class Two_Factor_Totp extends Two_Factor_Provider {
463
  */
464
  public static function base32_decode( $base32_string ) {
465
 
466
- $base32_string = strtoupper( $base32_string );
467
 
468
- if ( ! preg_match( '/^[' . self::$_base_32_chars . ']+$/', $base32_string, $match ) ) {
469
  throw new Exception( 'Invalid characters in the base32 string.' );
470
  }
471
 
472
- $l = strlen( $base32_string );
473
- $n = 0;
474
- $j = 0;
475
  $binary = '';
476
 
477
  for ( $i = 0; $i < $l; $i++ ) {
478
 
479
- $n = $n << 5; // Move buffer left by 5 to make room.
480
- $n = $n + strpos( self::$_base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
481
  $j += 5; // Keep track of number of bits in buffer.
482
 
483
  if ( $j >= 8 ) {
484
- $j -= 8;
485
  $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
486
  }
487
  }
@@ -503,6 +573,6 @@ class Two_Factor_Totp extends Two_Factor_Provider {
503
  if ( $a === $b ) {
504
  return 0;
505
  }
506
- return ($a < $b) ? -1 : 1;
507
  }
508
  }
24
  */
25
  const NOTICES_META_KEY = '_two_factor_totp_notices';
26
 
27
+ /**
28
+ * Action name for resetting the secret token.
29
+ *
30
+ * @var string
31
+ */
32
+ const ACTION_SECRET_DELETE = 'totp-delete';
33
+
34
+ const DEFAULT_KEY_BIT_SIZE = 160;
35
+ const DEFAULT_CRYPTO = 'sha1';
36
+ const DEFAULT_DIGIT_COUNT = 6;
37
+ const DEFAULT_TIME_STEP_SEC = 30;
38
  const DEFAULT_TIME_STEP_ALLOWANCE = 4;
39
+
40
+ /**
41
+ * Characters used in base32 encoding.
42
+ *
43
+ * @var string
44
+ */
45
+ private static $base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
46
 
47
  /**
48
  * Class constructor. Sets up hooks, etc.
49
+ *
50
+ * @codeCoverageIgnore
51
  */
52
  protected function __construct() {
53
+ add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_two_factor_options' ) );
54
+ add_action( 'personal_options_update', array( $this, 'user_two_factor_options_update' ) );
55
+ add_action( 'edit_user_profile_update', array( $this, 'user_two_factor_options_update' ) );
56
+ add_action( 'two_factor_user_settings_action', array( $this, 'user_settings_action' ), 10, 2 );
57
+
58
  return parent::__construct();
59
  }
60
 
61
  /**
62
  * Ensures only one instance of this class exists in memory at any one time.
63
+ *
64
+ * @codeCoverageIgnore
65
  */
66
  public static function get_instance() {
67
  static $instance;
75
  * Returns the name of the provider.
76
  */
77
  public function get_label() {
78
+ return _x( 'Time Based One-Time Password (TOTP)', 'Provider Label', 'two-factor' );
79
+ }
80
+
81
+ /**
82
+ * Trigger our custom user settings actions.
83
+ *
84
+ * @param integer $user_id User ID.
85
+ * @param string $action Action ID.
86
+ *
87
+ * @return void
88
+ *
89
+ * @codeCoverageIgnore
90
+ */
91
+ public function user_settings_action( $user_id, $action ) {
92
+ if ( self::ACTION_SECRET_DELETE === $action ) {
93
+ $this->delete_user_totp_key( $user_id );
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get the URL for deleting the secret token.
99
+ *
100
+ * @param integer $user_id User ID.
101
+ *
102
+ * @return string
103
+ *
104
+ * @codeCoverageIgnore
105
+ */
106
+ protected function get_token_delete_url_for_user( $user_id ) {
107
+ return Two_Factor_Core::get_user_update_action_url( $user_id, self::ACTION_SECRET_DELETE );
108
  }
109
 
110
  /**
112
  *
113
  * @param WP_User $user The current user being edited.
114
  * @return false
115
+ *
116
+ * @codeCoverageIgnore
117
  */
118
  public function user_two_factor_options( $user ) {
119
  if ( ! isset( $user->ID ) ) {
123
  wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false );
124
 
125
  $key = $this->get_user_totp_key( $user->ID );
126
+ $this->admin_notices( $user->ID );
127
 
128
  ?>
129
  <div id="two-factor-totp-options">
130
+ <?php
131
+ if ( empty( $key ) ) :
132
+ $key = $this->generate_key();
133
+ $site_name = get_bloginfo( 'name', 'display' );
134
+ $totp_title = apply_filters( 'two_factor_totp_title', $site_name . ':' . $user->user_login, $user );
135
  ?>
136
  <p>
137
  <?php esc_html_e( 'Please scan the QR code or manually enter the key, then enter an authentication code from your app in order to complete setup.', 'two-factor' ); ?>
138
  </p>
139
  <p>
140
+ <img src="<?php echo esc_url( $this->get_google_qr_code( $totp_title, $key, $site_name ) ); ?>" id="two-factor-totp-qrcode" />
141
  </p>
142
  <p>
143
  <code><?php echo esc_html( $key ); ?></code>
152
  </p>
153
  <?php else : ?>
154
  <p class="success">
155
+ <?php esc_html_e( 'Secret key is configured and registered. It is not possible to view it again for security reasons.', 'two-factor' ); ?>
156
  </p>
157
  <p>
158
+ <a class="button" href="<?php echo esc_url( self::get_token_delete_url_for_user( $user->ID ) ); ?>"><?php esc_html_e( 'Reset Key', 'two-factor' ); ?></a>
159
  <em class="description">
160
  <?php esc_html_e( 'You will have to re-scan the QR code on all devices as the previous codes will stop working.', 'two-factor' ); ?>
161
  </em>
169
  * Save the options specified in `::user_two_factor_options()`
170
  *
171
  * @param integer $user_id The user ID whose options are being updated.
172
+ *
173
+ * @return void
174
+ *
175
+ * @codeCoverageIgnore
176
  */
177
  public function user_two_factor_options_update( $user_id ) {
178
  $notices = array();
179
+ $errors = array();
 
 
180
 
181
  if ( isset( $_POST['_nonce_user_two_factor_totp_options'] ) ) {
182
  check_admin_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' );
183
 
 
 
 
 
 
184
  // Validate and store a new secret key.
185
  if ( ! empty( $_POST['two-factor-totp-authcode'] ) && ! empty( $_POST['two-factor-totp-key'] ) ) {
186
+ // Don't use filter_input() because we can't mock it during tests for now.
187
+ $authcode = filter_var( sanitize_text_field( $_POST['two-factor-totp-authcode'] ), FILTER_SANITIZE_NUMBER_INT );
188
+ $key = sanitize_text_field( $_POST['two-factor-totp-key'] );
189
+
190
+ if ( $this->is_valid_key( $key ) ) {
191
+ if ( $this->is_valid_authcode( $key, $authcode ) ) {
192
+ if ( ! $this->set_user_totp_key( $user_id, $key ) ) {
193
  $errors[] = __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' );
194
  }
195
  } else {
252
  * @return boolean
253
  */
254
  public function is_valid_key( $key ) {
255
+ $check = sprintf( '/^[%s]+$/', self::$base_32_chars );
256
 
257
  if ( 1 === preg_match( $check, $key ) ) {
258
  return true;
263
 
264
  /**
265
  * Display any available admin notices.
266
+ *
267
+ * @param integer $user_id User ID.
268
+ *
269
+ * @return void
270
+ *
271
+ * @codeCoverageIgnore
272
  */
273
+ public function admin_notices( $user_id ) {
274
+ $notices = get_user_meta( $user_id, self::NOTICES_META_KEY, true );
275
 
276
  if ( ! empty( $notices ) ) {
277
+ delete_user_meta( $user_id, self::NOTICES_META_KEY );
278
+
279
  foreach ( $notices as $class => $messages ) {
280
  ?>
281
+ <div class="<?php echo esc_attr( $class ); ?>">
282
  <?php
283
  foreach ( $messages as $msg ) {
284
  ?>
300
  * @param WP_User $user WP_User object of the logged-in user.
301
  *
302
  * @return bool Whether the user gave a valid code
303
+ *
304
+ * @codeCoverageIgnore
305
  */
306
  public function validate_authentication( $user ) {
307
+ if ( ! empty( $_REQUEST['authcode'] ) ) {
308
  return $this->is_valid_authcode(
309
  $this->get_user_totp_key( $user->ID ),
310
+ sanitize_text_field( $_REQUEST['authcode'] )
311
  );
312
  }
313
 
329
  * Ticks are the allowed offset from the correct time in 30 second increments,
330
  * so the default of 4 allows codes that are two minutes to either side of server time
331
  *
332
+ * @deprecated 0.7.0 Use {@see 'two_factor_totp_time_step_allowance'} instead.
333
  * @param int $max_ticks Max ticks of time correction to allow. Default 4.
334
  */
335
+ $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' );
336
+
337
+ $max_ticks = apply_filters( 'two_factor_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
338
 
339
  // Array of all ticks to allow, sorted using absolute value to test closest match first.
340
  $ticks = range( - $max_ticks, $max_ticks );
344
 
345
  foreach ( $ticks as $offset ) {
346
  $log_time = $time + $offset;
347
+ if ( hash_equals(self::calc_totp( $key, $log_time ), $authcode ) ) {
348
  return true;
349
  }
350
  }
359
  * @return string $bitsize long string composed of available base32 chars.
360
  */
361
  public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
362
+ $bytes = ceil( $bitsize / 8 );
363
  $secret = wp_generate_password( $bytes, true, true );
364
 
365
  return self::base32_encode( $secret );
377
  if ( PHP_INT_SIZE >= 8 ) {
378
  // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
379
  if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
380
+ return pack( 'J', $value ); // phpcs:ignore PHPCompatibility.ParameterValues.NewPackFormat.NewFormatFound
381
  }
382
  $highmap = 0xffffffff << 32;
383
  $higher = ( $value & $highmap ) >> 32;
389
  $higher = 0;
390
  }
391
 
392
+ $lowmap = 0xffffffff;
393
+ $lower = $value & $lowmap;
394
 
395
  return pack( 'NN', $higher, $lower );
396
  }
437
  * @param string $title The title to display in the Authentication app.
438
  *
439
  * @return string A URL to use as an img src to display the QR code
440
+ *
441
+ * @codeCoverageIgnore
442
  */
443
  public static function get_google_qr_code( $name, $key, $title = null ) {
444
  // Encode to support spaces, question marks and other characters.
445
+ $name = rawurlencode( $name );
446
  $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key );
447
  if ( isset( $title ) ) {
448
  $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) );
468
  * Prints the form that prompts the user to authenticate.
469
  *
470
  * @param WP_User $user WP_User object of the logged-in user.
471
+ *
472
+ * @codeCoverageIgnore
473
  */
474
  public function authentication_page( $user ) {
475
+ require_once ABSPATH . '/wp-admin/includes/template.php';
476
  ?>
477
+ <p>
478
+ <?php esc_html_e( 'Please enter the code generated by your authenticator app.', 'two-factor' ); ?>
479
+ </p>
480
  <p>
481
  <label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
482
+ <input type="tel" autocomplete="one-time-code" name="authcode" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
483
  </p>
484
  <script type="text/javascript">
485
  setTimeout( function(){
486
  var d;
487
  try{
488
  d = document.getElementById('authcode');
 
489
  d.focus();
490
  } catch(e){}
491
  }, 200);
513
  }
514
 
515
  $five_bit_sections = str_split( $binary_string, 5 );
516
+ $base32_string = '';
517
 
518
  foreach ( $five_bit_sections as $five_bit_section ) {
519
+ $base32_string .= self::$base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
520
  }
521
 
522
  return $base32_string;
533
  */
534
  public static function base32_decode( $base32_string ) {
535
 
536
+ $base32_string = strtoupper( $base32_string );
537
 
538
+ if ( ! preg_match( '/^[' . self::$base_32_chars . ']+$/', $base32_string, $match ) ) {
539
  throw new Exception( 'Invalid characters in the base32 string.' );
540
  }
541
 
542
+ $l = strlen( $base32_string );
543
+ $n = 0;
544
+ $j = 0;
545
  $binary = '';
546
 
547
  for ( $i = 0; $i < $l; $i++ ) {
548
 
549
+ $n = $n << 5; // Move buffer left by 5 to make room.
550
+ $n = $n + strpos( self::$base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
551
  $j += 5; // Keep track of number of bits in buffer.
552
 
553
  if ( $j >= 8 ) {
554
+ $j -= 8;
555
  $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
556
  }
557
  }
573
  if ( $a === $b ) {
574
  return 0;
575
  }
576
+ return ( $a < $b ) ? -1 : 1;
577
  }
578
  }
providers/css/fido-u2f-admin.css CHANGED
@@ -1,9 +1,11 @@
1
  #security-keys-section .wp-list-table {
2
  margin-bottom: 2em;
3
  }
 
4
  #security-keys-section .register-security-key .spinner {
5
  float: none;
6
  }
 
7
  #security-keys-section .security-key-status {
8
  vertical-align: middle;
9
  font-style: italic;
1
  #security-keys-section .wp-list-table {
2
  margin-bottom: 2em;
3
  }
4
+
5
  #security-keys-section .register-security-key .spinner {
6
  float: none;
7
  }
8
+
9
  #security-keys-section .security-key-status {
10
  vertical-align: middle;
11
  font-style: italic;
providers/js/fido-u2f-admin-inline-edit.js CHANGED
@@ -1,4 +1,4 @@
1
- /* global inlineEditL10n, ajaxurl */
2
  var inlineEditKey;
3
 
4
  ( function( $ ) {
@@ -39,7 +39,12 @@ var inlineEditKey;
39
 
40
  toggle: function( el ) {
41
  var t = this;
42
- 'none' === $( t.what + t.getId( el ) ).css( 'display' ) ? t.revert() : t.edit( el );
 
 
 
 
 
43
  },
44
 
45
  edit: function( id ) {
@@ -51,7 +56,9 @@ var inlineEditKey;
51
  id = t.getId( id );
52
  }
53
 
54
- editRow = $( '#inline-edit' ).clone( true ), rowData = $( '#inline_' + id );
 
 
55
  $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '#security-keys-section .widefat thead' ).length );
56
 
57
  $( t.what + id ).hide().after( editRow ).after( '<tr class="hidden"></tr>' );
@@ -80,7 +87,8 @@ var inlineEditKey;
80
 
81
  params = {
82
  action: 'inline-save-key',
83
- keyHandle: id
 
84
  };
85
 
86
  fields = $( '#edit-' + id ).find( ':input' ).serialize();
@@ -89,7 +97,7 @@ var inlineEditKey;
89
  // Make ajax request.
90
  $.post( ajaxurl, params,
91
  function( r ) {
92
- var row, newID, optionValue;
93
  $( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' );
94
 
95
  if ( r ) {
@@ -100,10 +108,8 @@ var inlineEditKey;
100
  $( '#edit-' + id ).before( r ).remove();
101
 
102
  if ( newID ) {
103
- optionValue = newID.replace( 'key-', '' );
104
  row = $( '#' + newID );
105
  } else {
106
- optionValue = id;
107
  row = $( inlineEditKey.what + id );
108
  }
109
 
@@ -141,5 +147,4 @@ var inlineEditKey;
141
  $( document ).ready( function() {
142
  inlineEditKey.init();
143
  } );
144
-
145
- } )( jQuery );
1
+ /* global window, document, jQuery, inlineEditL10n, ajaxurl */
2
  var inlineEditKey;
3
 
4
  ( function( $ ) {
39
 
40
  toggle: function( el ) {
41
  var t = this;
42
+
43
+ if ( 'none' === $( t.what + t.getId( el ) ).css( 'display' ) ) {
44
+ t.revert();
45
+ } else {
46
+ t.edit( el );
47
+ }
48
  },
49
 
50
  edit: function( id ) {
56
  id = t.getId( id );
57
  }
58
 
59
+ editRow = $( '#inline-edit' ).clone( true );
60
+ rowData = $( '#inline_' + id );
61
+
62
  $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '#security-keys-section .widefat thead' ).length );
63
 
64
  $( t.what + id ).hide().after( editRow ).after( '<tr class="hidden"></tr>' );
87
 
88
  params = {
89
  action: 'inline-save-key',
90
+ keyHandle: id,
91
+ user_id: window.u2fL10n.user_id
92
  };
93
 
94
  fields = $( '#edit-' + id ).find( ':input' ).serialize();
97
  // Make ajax request.
98
  $.post( ajaxurl, params,
99
  function( r ) {
100
+ var row, newID;
101
  $( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' );
102
 
103
  if ( r ) {
108
  $( '#edit-' + id ).before( r ).remove();
109
 
110
  if ( newID ) {
 
111
  row = $( '#' + newID );
112
  } else {
 
113
  row = $( inlineEditKey.what + id );
114
  }
115
 
147
  $( document ).ready( function() {
148
  inlineEditKey.init();
149
  } );
150
+ }( jQuery ) );
 
providers/js/fido-u2f-admin.js CHANGED
@@ -1,8 +1,8 @@
1
- /* global u2f, u2fL10n */
2
  ( function( $ ) {
3
  var $button = $( '#register_security_key' );
4
  var $statusNotice = $( '#security-keys-section .security-key-status' );
5
- var u2fSupported = ( u2f && u2f.HasNativeApiSupport );
6
 
7
  if ( ! u2fSupported ) {
8
  $statusNotice.text( u2fL10n.text.u2f_not_supported );
@@ -24,7 +24,7 @@
24
  challenge: u2fL10n.register.request.challenge
25
  };
26
 
27
- u2f.register( u2fL10n.register.request.appId, [ registerRequest ], u2fL10n.register.sigs, function( data ) {
28
  $( '.register-security-key .spinner' ).removeClass( 'is-active' );
29
  $button.prop( 'disabled', false );
30
 
@@ -45,4 +45,4 @@
45
  $( '<form>' )[0].submit.call( $( '#your-profile' )[0] );
46
  } );
47
  } );
48
- } )( jQuery );
1
+ /* global window, u2fL10n, jQuery */
2
  ( function( $ ) {
3
  var $button = $( '#register_security_key' );
4
  var $statusNotice = $( '#security-keys-section .security-key-status' );
5
+ var u2fSupported = ( window.u2f && 'register' in window.u2f );
6
 
7
  if ( ! u2fSupported ) {
8
  $statusNotice.text( u2fL10n.text.u2f_not_supported );
24
  challenge: u2fL10n.register.request.challenge
25
  };
26
 
27
+ window.u2f.register( u2fL10n.register.request.appId, [ registerRequest ], u2fL10n.register.sigs, function( data ) {
28
  $( '.register-security-key .spinner' ).removeClass( 'is-active' );
29
  $button.prop( 'disabled', false );
30
 
45
  $( '<form>' )[0].submit.call( $( '#your-profile' )[0] );
46
  } );
47
  } );
48
+ }( jQuery ) );
providers/js/fido-u2f-login.js CHANGED
@@ -1,4 +1,4 @@
1
- /* global u2f, u2fL10n */
2
  ( function( $ ) {
3
  if ( ! window.u2fL10n ) {
4
  window.console.error( 'u2fL10n is not defined' );
@@ -13,4 +13,4 @@
13
  $( '#loginform' ).submit();
14
  }
15
  } );
16
- } )( jQuery );
1
+ /* global window, u2f, u2fL10n, jQuery */
2
  ( function( $ ) {
3
  if ( ! window.u2fL10n ) {
4
  window.console.error( 'u2fL10n is not defined' );
13
  $( '#loginform' ).submit();
14
  }
15
  } );
16
+ }( jQuery ) );
readme.md DELETED
@@ -1,33 +0,0 @@
1
- <!-- DO NOT EDIT THIS FILE; it is auto-generated from readme.txt -->
2
- # Two-Factor
3
-
4
- ![Banner](assets/banner-1544x500.png)
5
- Enable Two-Factor Authentication using time-based one-time passwords (OTP, Google Authenticator), Universal 2nd Factor (FIDO U2F, YubiKey), email and backup verification codes.
6
-
7
- **Contributors:** [georgestephanis](https://profiles.wordpress.org/georgestephanis), [valendesigns](https://profiles.wordpress.org/valendesigns), [stevenkword](https://profiles.wordpress.org/stevenkword), [extendwings](https://profiles.wordpress.org/extendwings), [sgrant](https://profiles.wordpress.org/sgrant), [aaroncampbell](https://profiles.wordpress.org/aaroncampbell), [johnbillion](https://profiles.wordpress.org/johnbillion), [stevegrunwell](https://profiles.wordpress.org/stevegrunwell), [netweb](https://profiles.wordpress.org/netweb), [kasparsd](https://profiles.wordpress.org/kasparsd)
8
- **Tags:** [two factor](https://wordpress.org/plugins/tags/two-factor), [two step](https://wordpress.org/plugins/tags/two-step), [authentication](https://wordpress.org/plugins/tags/authentication), [login](https://wordpress.org/plugins/tags/login), [totp](https://wordpress.org/plugins/tags/totp), [fido u2f](https://wordpress.org/plugins/tags/fido-u2f), [u2f](https://wordpress.org/plugins/tags/u2f), [email](https://wordpress.org/plugins/tags/email), [backup codes](https://wordpress.org/plugins/tags/backup-codes), [2fa](https://wordpress.org/plugins/tags/2fa), [yubikey](https://wordpress.org/plugins/tags/yubikey)
9
- **Requires at least:** 4.3
10
- **Tested up to:** 5.1
11
- **Stable tag:** trunk (master)
12
-
13
- [![Build Status](https://travis-ci.org/WordPress/two-factor.svg?branch=master)](https://travis-ci.org/WordPress/two-factor) [![Coverage Status](https://coveralls.io/repos/WordPress/two-factor/badge.svg?branch=master)](https://coveralls.io/github/WordPress/two-factor) [![Built with Grunt](https://gruntjs.com/cdn/builtwith.svg)](http://gruntjs.com)
14
-
15
- ## Description ##
16
-
17
- For more history, see [this post](https://stephanis.info/2013/08/14/two-cents-on-two-factor/).
18
-
19
- ## Get Involved ##
20
-
21
- Development happens [on GitHub](https://github.com/georgestephanis/two-factor/). Join the `#core-passwords` channel [on WordPress Slack](http://wordpress.slack.com) ([sign up here](http://chat.wordpress.org)).
22
-
23
- Here is how to get started:
24
-
25
- $ git clone https://github.com/georgestephanis/two-factor.git
26
- $ npm install
27
-
28
- Then open [a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) with the suggested changes.
29
-
30
- ## Changelog ##
31
-
32
- See the [release history](https://github.com/georgestephanis/two-factor/releases).
33
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
readme.txt CHANGED
@@ -1,27 +1,44 @@
1
- === Two-Factor ===
2
- Contributors: georgestephanis, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, kasparsd
3
- Tags: two factor, two step, authentication, login, totp, fido u2f, u2f, email, backup codes, 2fa, yubikey
4
- Requires at least: 4.3
5
- Tested up to: 5.1
6
- Stable tag: trunk
7
-
8
- Enable Two-Factor Authentication using time-based one-time passwords (OTP, Google Authenticator), Universal 2nd Factor (FIDO U2F, YubiKey), email and backup verification codes.
9
-
10
- == Description ==
11
-
12
- For more history, see [this post](https://stephanis.info/2013/08/14/two-cents-on-two-factor/).
13
-
14
- == Get Involved ==
15
-
16
- Development happens [on GitHub](https://github.com/georgestephanis/two-factor/). Join the `#core-passwords` channel [on WordPress Slack](http://wordpress.slack.com) ([sign up here](http://chat.wordpress.org)).
17
-
18
- Here is how to get started:
19
-
20
- $ git clone https://github.com/georgestephanis/two-factor.git
21
- $ npm install
22
-
23
- Then open [a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) with the suggested changes.
24
-
25
- == Changelog ==
26
-
27
- See the [release history](https://github.com/georgestephanis/two-factor/releases).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === Two-Factor ===
2
+ Contributors: georgestephanis, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, kasparsd, alihusnainarshad, passoniate
3
+ Tags: two factor, two step, authentication, login, totp, fido u2f, u2f, email, backup codes, 2fa, yubikey
4
+ Requires at least: 4.3
5
+ Tested up to: 6.0
6
+ Requires PHP: 5.6
7
+ Stable tag: 0.7.3
8
+
9
+ Enable Two-Factor Authentication using time-based one-time passwords (OTP, Google Authenticator), Universal 2nd Factor (FIDO U2F, YubiKey), email and backup verification codes.
10
+
11
+ == Description ==
12
+
13
+ Use the "Two-Factor Options" section under "Users" → "Your Profile" to enable and configure one or multiple two-factor authentication providers for your account:
14
+
15
+ - Email codes
16
+ - Time Based One-Time Passwords (TOTP)
17
+ - FIDO Universal 2nd Factor (U2F)
18
+ - Backup Codes
19
+ - Dummy Method (only for testing purposes)
20
+
21
+ For more history, see [this post](https://georgestephanis.wordpress.com/2013/08/14/two-cents-on-two-factor/).
22
+
23
+ = Actions & Filters =
24
+
25
+ Here is a list of action and filter hooks provided by the plugin:
26
+
27
+ - `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.
28
+ - `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.
29
+ - `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.
30
+ - `two_factor_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.
31
+
32
+ == Screenshots ==
33
+
34
+ 1. Two-factor options under User Profile.
35
+ 2. U2F Security Keys section under User Profile.
36
+ 3. Email Code Authentication during WordPress Login.
37
+
38
+ == Get Involved ==
39
+
40
+ Development happens [on GitHub](https://github.com/wordpress/two-factor/).
41
+
42
+ == Changelog ==
43
+
44
+ See the [release history](https://github.com/wordpress/two-factor/releases).
two-factor.php CHANGED
@@ -1,11 +1,19 @@
1
  <?php
2
  /**
 
 
 
 
 
 
 
 
3
  * Plugin Name: Two Factor
4
  * Plugin URI: https://wordpress.org/plugins/two-factor/
5
- * Description: A prototype extensible core to enable Two-Factor Authentication.
6
- * Author: George Stephanis
7
- * Version: 0.4.0
8
- * Author URI: https://stephanis.info
9
  * Network: True
10
  * Text Domain: two-factor
11
  */
@@ -15,14 +23,26 @@
15
  */
16
  define( 'TWO_FACTOR_DIR', plugin_dir_path( __FILE__ ) );
17
 
 
 
 
 
 
18
  /**
19
  * Include the base class here, so that other plugins can also extend it.
20
  */
21
- require_once( TWO_FACTOR_DIR . 'providers/class.two-factor-provider.php' );
22
 
23
  /**
24
  * Include the core that handles the common bits.
25
  */
26
- require_once( TWO_FACTOR_DIR . 'class.two-factor-core.php' );
 
 
 
 
 
 
 
27
 
28
- Two_Factor_Core::add_hooks();
1
  <?php
2
  /**
3
+ * Two Factor
4
+ *
5
+ * @package Two_Factor
6
+ * @author Plugin Contributors
7
+ * @copyright 2020 Plugin Contributors
8
+ * @license GPL-2.0-or-later
9
+ *
10
+ * @wordpress-plugin
11
  * Plugin Name: Two Factor
12
  * Plugin URI: https://wordpress.org/plugins/two-factor/
13
+ * Description: Two-Factor Authentication using time-based one-time passwords, Universal 2nd Factor (FIDO U2F), email and backup verification codes.
14
+ * Author: Plugin Contributors
15
+ * Version: 0.7.3
16
+ * Author URI: https://github.com/wordpress/two-factor/graphs/contributors
17
  * Network: True
18
  * Text Domain: two-factor
19
  */
23
  */
24
  define( 'TWO_FACTOR_DIR', plugin_dir_path( __FILE__ ) );
25
 
26
+ /**
27
+ * Version of the plugin.
28
+ */
29
+ define( 'TWO_FACTOR_VERSION', '0.7.3' );
30
+
31
  /**
32
  * Include the base class here, so that other plugins can also extend it.
33
  */
34
+ require_once TWO_FACTOR_DIR . 'providers/class-two-factor-provider.php';
35
 
36
  /**
37
  * Include the core that handles the common bits.
38
  */
39
+ require_once TWO_FACTOR_DIR . 'class-two-factor-core.php';
40
+
41
+ /**
42
+ * A compatability layer for some of the most-used plugins out there.
43
+ */
44
+ require_once TWO_FACTOR_DIR . 'class-two-factor-compat.php';
45
+
46
+ $two_factor_compat = new Two_Factor_Compat();
47
 
48
+ Two_Factor_Core::add_hooks( $two_factor_compat );
user-edit.css CHANGED
@@ -34,4 +34,9 @@
34
 
35
  .two-factor-methods-table tbody tr:nth-child(odd) {
36
  background-color: #f9f9f9;
37
- }
 
 
 
 
 
34
 
35
  .two-factor-methods-table tbody tr:nth-child(odd) {
36
  background-color: #f9f9f9;
37
+ }
38
+
39
+ .two-factor-methods-table .two-factor-method-label {
40
+ display: block;
41
+ font-weight: 700;
42
+ }