Two-Factor - Version 0.7.2

Version Description

Download this release

Release Info

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

Version 0.7.2

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
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_SANITIZE_STRING );
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 ADDED
@@ -0,0 +1,1072 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Two Factore Core Class.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
+ /**
9
+ * Class for creating two factor authorization.
10
+ *
11
+ * @since 0.1-dev
12
+ *
13
+ * @package Two_Factor
14
+ */
15
+ class Two_Factor_Core {
16
+
17
+ /**
18
+ * The user meta provider key.
19
+ *
20
+ * @type string
21
+ */
22
+ const PROVIDER_USER_META_KEY = '_two_factor_provider';
23
+
24
+ /**
25
+ * The user meta enabled providers key.
26
+ *
27
+ * @type string
28
+ */
29
+ const ENABLED_PROVIDERS_USER_META_KEY = '_two_factor_enabled_providers';
30
+
31
+ /**
32
+ * The user meta nonce key.
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 );
71
+ add_action( 'login_form_validate_2fa', array( __CLASS__, 'login_form_validate_2fa' ) );
72
+ add_action( 'login_form_backup_2fa', array( __CLASS__, 'backup_2fa' ) );
73
+ add_action( 'show_user_profile', array( __CLASS__, 'user_two_factor_options' ) );
74
+ add_action( 'edit_user_profile', array( __CLASS__, 'user_two_factor_options' ) );
75
+ add_action( 'personal_options_update', array( __CLASS__, 'user_two_factor_options_update' ) );
76
+ add_action( 'edit_user_profile_update', array( __CLASS__, 'user_two_factor_options_update' ) );
77
+ add_filter( 'manage_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) );
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
+ /**
100
+ * Loads the plugin's text domain.
101
+ *
102
+ * Sites on WordPress 4.6+ benefit from just-in-time loading of translations.
103
+ */
104
+ public static function load_textdomain() {
105
+ load_plugin_textdomain( 'two-factor' );
106
+ }
107
+
108
+ /**
109
+ * For each provider, include it and then instantiate it.
110
+ *
111
+ * @since 0.1-dev
112
+ *
113
+ * @return array
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
+ /**
125
+ * Filter the supplied providers.
126
+ *
127
+ * This lets third-parties either remove providers (such as Email), or
128
+ * add their own providers (such as text message or Clef).
129
+ *
130
+ * @param array $providers A key-value array where the key is the class name, and
131
+ * the value is the path to the file containing the class.
132
+ */
133
+ $providers = apply_filters( 'two_factor_providers', $providers );
134
+
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.
155
+ */
156
+ if ( class_exists( $class ) ) {
157
+ try {
158
+ $providers[ $class ] = call_user_func( array( $class, 'get_instance' ) );
159
+ } catch ( Exception $e ) {
160
+ unset( $providers[ $class ] );
161
+ }
162
+ }
163
+ }
164
+
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_SANITIZE_STRING );
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_SANITIZE_STRING );
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
+ *
314
+ * @param WP_User $user WP_User object of the logged-in user.
315
+ * @return array
316
+ */
317
+ public static function get_enabled_providers_for_user( $user = null ) {
318
+ if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) {
319
+ $user = wp_get_current_user();
320
+ }
321
+
322
+ $providers = self::get_providers();
323
+ $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true );
324
+ if ( empty( $enabled_providers ) ) {
325
+ $enabled_providers = array();
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
+ /**
339
+ * Get all Two-Factor Auth providers that are both enabled and configured for the specified|current user.
340
+ *
341
+ * @param WP_User $user WP_User object of the logged-in user.
342
+ * @return array
343
+ */
344
+ public static function get_available_providers_for_user( $user = null ) {
345
+ if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) {
346
+ $user = wp_get_current_user();
347
+ }
348
+
349
+ $providers = self::get_providers();
350
+ $enabled_providers = self::get_enabled_providers_for_user( $user );
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
+ }
358
+
359
+ return $configured_providers;
360
+ }
361
+
362
+ /**
363
+ * Gets the Two-Factor Auth provider for the specified|current user.
364
+ *
365
+ * @since 0.1-dev
366
+ *
367
+ * @param int $user_id Optional. User ID. Default is 'null'.
368
+ * @return object|null
369
+ */
370
+ public static function get_primary_provider_for_user( $user_id = null ) {
371
+ if ( empty( $user_id ) || ! is_numeric( $user_id ) ) {
372
+ $user_id = get_current_user_id();
373
+ }
374
+
375
+ $providers = self::get_providers();
376
+ $available_providers = self::get_available_providers_for_user( get_userdata( $user_id ) );
377
+
378
+ // If there's only one available provider, force that to be the primary.
379
+ if ( empty( $available_providers ) ) {
380
+ return null;
381
+ } elseif ( 1 === count( $available_providers ) ) {
382
+ $provider = key( $available_providers );
383
+ } else {
384
+ $provider = get_user_meta( $user_id, self::PROVIDER_USER_META_KEY, true );
385
+
386
+ // If the provider specified isn't enabled, just grab the first one that is.
387
+ if ( ! isset( $available_providers[ $provider ] ) ) {
388
+ $provider = key( $available_providers );
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Filter the two-factor authentication provider used for this user.
394
+ *
395
+ * @param string $provider The provider currently being used.
396
+ * @param int $user_id The user ID.
397
+ */
398
+ $provider = apply_filters( 'two_factor_primary_provider_for_user', $provider, $user_id );
399
+
400
+ if ( isset( $providers[ $provider ] ) ) {
401
+ return $providers[ $provider ];
402
+ }
403
+
404
+ return null;
405
+ }
406
+
407
+ /**
408
+ * Quick boolean check for whether a given user is using two-step.
409
+ *
410
+ * @since 0.1-dev
411
+ *
412
+ * @param int $user_id Optional. User ID. Default is 'null'.
413
+ * @return bool
414
+ */
415
+ public static function is_user_using_two_factor( $user_id = null ) {
416
+ $provider = self::get_primary_provider_for_user( $user_id );
417
+ return ! empty( $provider );
418
+ }
419
+
420
+ /**
421
+ * Handle the browser-based login.
422
+ *
423
+ * @since 0.1-dev
424
+ *
425
+ * @param string $user_login Username.
426
+ * @param WP_User $user WP_User object of the logged-in user.
427
+ */
428
+ public static function wp_login( $user_login, $user ) {
429
+ if ( ! self::is_user_using_two_factor( $user->ID ) ) {
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.
465
+ *
466
+ * @param WP_User|WP_Error $user Valid WP_User only if the previous filters
467
+ * have verified and confirmed the
468
+ * authentication credentials.
469
+ *
470
+ * @return WP_User|WP_Error
471
+ */
472
+ public static function filter_authenticate( $user ) {
473
+ if ( $user instanceof WP_User && self::is_api_request() && self::is_user_using_two_factor( $user->ID ) && ! self::is_user_api_login_enabled( $user->ID ) ) {
474
+ return new WP_Error(
475
+ 'invalid_application_credentials',
476
+ __( 'Error: API login for user disabled.', 'two-factor' )
477
+ );
478
+ }
479
+
480
+ return $user;
481
+ }
482
+
483
+ /**
484
+ * If the current user can login via API requests such as XML-RPC and REST.
485
+ *
486
+ * @param integer $user_id User ID.
487
+ *
488
+ * @return boolean
489
+ */
490
+ public static function is_user_api_login_enabled( $user_id ) {
491
+ return (bool) apply_filters( 'two_factor_user_api_login_enable', false, $user_id );
492
+ }
493
+
494
+ /**
495
+ * Is the current request an XML-RPC or REST request.
496
+ *
497
+ * @return boolean
498
+ */
499
+ public static function is_api_request() {
500
+ if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
501
+ return true;
502
+ }
503
+
504
+ if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
505
+ return true;
506
+ }
507
+
508
+ return false;
509
+ }
510
+
511
+ /**
512
+ * Display the login form.
513
+ *
514
+ * @since 0.1-dev
515
+ *
516
+ * @param WP_User $user WP_User object of the logged-in user.
517
+ */
518
+ public static function show_two_factor_login( $user ) {
519
+ if ( ! $user ) {
520
+ $user = wp_get_current_user();
521
+ }
522
+
523
+ $login_nonce = self::create_login_nonce( $user->ID );
524
+ if ( ! $login_nonce ) {
525
+ wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) );
526
+ }
527
+
528
+ $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : admin_url();
529
+
530
+ self::login_html( $user, $login_nonce['key'], $redirect_to );
531
+ }
532
+
533
+ /**
534
+ * Display the Backup code 2fa screen.
535
+ *
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_SANITIZE_STRING );
541
+ $provider = filter_input( INPUT_GET, 'provider', FILTER_SANITIZE_STRING );
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
+ }
569
+
570
+ /**
571
+ * Generates the html form for the second step of the authentication process.
572
+ *
573
+ * @since 0.1-dev
574
+ *
575
+ * @param WP_User $user WP_User object of the logged-in user.
576
+ * @param string $login_nonce A string nonce stored in usermeta.
577
+ * @param string $redirect_to The URL to which the user would like to be redirected.
578
+ * @param string $error_msg Optional. Login error message.
579
+ * @param string|object $provider An override to the provider.
580
+ */
581
+ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg = '', $provider = null ) {
582
+ if ( empty( $provider ) ) {
583
+ $provider = self::get_primary_provider_for_user( $user->ID );
584
+ } elseif ( is_string( $provider ) && method_exists( $provider, 'get_instance' ) ) {
585
+ $provider = call_user_func( array( $provider, 'get_instance' ) );
586
+ }
587
+
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();
602
+
603
+ if ( ! empty( $error_msg ) ) {
604
+ echo '<div id="login_error"><strong>' . esc_html( $error_msg ) . '</strong><br /></div>';
605
+ }
606
+ ?>
607
+
608
+ <form name="validate_2fa_form" id="loginform" action="<?php echo esc_url( self::login_url( array( 'action' => 'validate_2fa' ), 'login_post' ) ); ?>" method="post" autocomplete="off">
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 ); ?>
620
+ </form>
621
+
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,
630
+ 'wp-auth-id' => $user->ID,
631
+ 'wp-auth-nonce' => $login_nonce,
632
+ 'redirect_to' => $redirect_to,
633
+ 'rememberme' => $rememberme,
634
+ )
635
+ );
636
+ ?>
637
+ <div class="backup-methods-wrap">
638
+ <p class="backup-methods">
639
+ <a href="<?php echo esc_url( $login_url ); ?>">
640
+ <?php
641
+ echo esc_html(
642
+ sprintf(
643
+ // translators: %s: Two-factor method name.
644
+ __( 'Or, use your backup method: %s &rarr;', 'two-factor' ),
645
+ $backup_provider->get_label()
646
+ )
647
+ );
648
+ ?>
649
+ </a>
650
+ </p>
651
+ </div>
652
+ <?php elseif ( 1 < count( $backup_providers ) ) : ?>
653
+ <div class="backup-methods-wrap">
654
+ <p class="backup-methods">
655
+ <a href="javascript:;" onclick="document.querySelector('ul.backup-methods').style.display = 'block';">
656
+ <?php esc_html_e( 'Or, use a backup method…', 'two-factor' ); ?>
657
+ </a>
658
+ </p>
659
+ <ul class="backup-methods">
660
+ <?php
661
+ foreach ( $backup_providers as $backup_classname => $backup_provider ) :
662
+ $login_url = self::login_url(
663
+ array(
664
+ 'action' => 'backup_2fa',
665
+ 'provider' => $backup_classname,
666
+ 'wp-auth-id' => $user->ID,
667
+ 'wp-auth-nonce' => $login_nonce,
668
+ 'redirect_to' => $redirect_to,
669
+ 'rememberme' => $rememberme,
670
+ )
671
+ );
672
+ ?>
673
+ <li>
674
+ <a href="<?php echo esc_url( $login_url ); ?>">
675
+ <?php $backup_provider->print_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
+
713
+ /**
714
+ * Generate the two-factor login form URL.
715
+ *
716
+ * @param array $params List of query argument pairs to add to the URL.
717
+ * @param string $scheme URL scheme context.
718
+ *
719
+ * @return string
720
+ */
721
+ public static function login_url( $params = array(), $scheme = 'login' ) {
722
+ if ( ! is_array( $params ) ) {
723
+ $params = array();
724
+ }
725
+
726
+ $params = urlencode_deep( $params );
727
+
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
+ *
745
+ * @since 0.1-dev
746
+ *
747
+ * @param int $user_id User ID.
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
+
769
+ return $login_nonce;
770
+ }
771
+
772
+ /**
773
+ * Delete the login nonce.
774
+ *
775
+ * @since 0.1-dev
776
+ *
777
+ * @param int $user_id User ID.
778
+ * @return bool
779
+ */
780
+ public static function delete_login_nonce( $user_id ) {
781
+ return delete_user_meta( $user_id, self::USER_META_NONCE_KEY );
782
+ }
783
+
784
+ /**
785
+ * Verify the login nonce.
786
+ *
787
+ * @since 0.1-dev
788
+ *
789
+ * @param int $user_id User ID.
790
+ * @param string $nonce Login nonce.
791
+ * @return bool
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
+ /**
811
+ * Login form validation.
812
+ *
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_SANITIZE_STRING );
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_SANITIZE_STRING );
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 );
843
+ }
844
+
845
+ // Allow the provider to re-send codes, etc.
846
+ if ( true === $provider->pre_process_authentication( $user ) ) {
847
+ $login_nonce = self::create_login_nonce( $user->ID );
848
+ if ( ! $login_nonce ) {
849
+ wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) );
850
+ }
851
+
852
+ self::login_html( $user, $login_nonce['key'], $_REQUEST['redirect_to'], '', $provider );
853
+ exit;
854
+ }
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 );
859
+
860
+ $login_nonce = self::create_login_nonce( $user->ID );
861
+ if ( ! $login_nonce ) {
862
+ wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) );
863
+ }
864
+
865
+ self::login_html( $user, $login_nonce['key'], $_REQUEST['redirect_to'], esc_html__( 'ERROR: Invalid verification code.', 'two-factor' ), $provider );
866
+ exit;
867
+ }
868
+
869
+ self::delete_login_nonce( $user->ID );
870
+
871
+ $rememberme = false;
872
+ if ( isset( $_REQUEST['rememberme'] ) && $_REQUEST['rememberme'] ) {
873
+ $rememberme = true;
874
+ }
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'] );
886
+ if ( $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
903
+ exit;
904
+ }
905
+ $redirect_to = apply_filters( 'login_redirect', $_REQUEST['redirect_to'], $_REQUEST['redirect_to'], $user );
906
+ wp_safe_redirect( $redirect_to );
907
+
908
+ exit;
909
+ }
910
+
911
+ /**
912
+ * Filter the columns on the Users admin screen.
913
+ *
914
+ * @param array $columns Available columns.
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
+
922
+ /**
923
+ * Output the 2FA column data on the Users screen.
924
+ *
925
+ * @param string $output The column output.
926
+ * @param string $column_name The column ID.
927
+ * @param int $user_id The user ID.
928
+ * @return string The column output.
929
+ */
930
+ public static function manage_users_custom_column( $output, $column_name, $user_id ) {
931
+
932
+ if ( 'two-factor' !== $column_name ) {
933
+ return $output;
934
+ }
935
+
936
+ if ( ! self::is_user_using_two_factor( $user_id ) ) {
937
+ return sprintf( '<span class="dashicons-before dashicons-no-alt">%s</span>', esc_html__( 'Disabled', 'two-factor' ) );
938
+ } else {
939
+ $provider = self::get_primary_provider_for_user( $user_id );
940
+ return esc_html( $provider->get_label() );
941
+ }
942
+
943
+ }
944
+
945
+ /**
946
+ * Add user profile fields.
947
+ *
948
+ * This executes during the `show_user_profile` & `edit_user_profile` actions.
949
+ *
950
+ * @since 0.1-dev
951
+ *
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 );
962
+ } else {
963
+ $primary_provider_key = null;
964
+ }
965
+
966
+ wp_nonce_field( 'user_two_factor_options', '_nonce_user_two_factor_options', false );
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">
977
+ <thead>
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( 'Name', '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 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
+ <?php
991
+ $object->print_label();
992
+
993
+ /**
994
+ * Fires after user options are shown.
995
+ *
996
+ * Use the {@see 'two_factor_user_options_' . $class } hook instead.
997
+ *
998
+ * @deprecated 0.7.0
999
+ *
1000
+ * @param WP_User $user The user.
1001
+ */
1002
+ do_action_deprecated( 'two-factor-user-options-' . $class, array( $user ), '0.7.0', 'two_factor_user_options_' . $class );
1003
+ do_action( 'two_factor_user_options_' . $class, $user );
1004
+ ?>
1005
+ </td>
1006
+ </tr>
1007
+ <?php endforeach; ?>
1008
+ </tbody>
1009
+ </table>
1010
+ </td>
1011
+ </tr>
1012
+ </table>
1013
+ <?php
1014
+ /**
1015
+ * Fires after the Two Factor methods table.
1016
+ *
1017
+ * To be used by Two Factor methods to add settings UI.
1018
+ *
1019
+ * @since 0.1-dev
1020
+ */
1021
+ do_action( 'show_user_security_settings', $user );
1022
+ }
1023
+
1024
+ /**
1025
+ * Update the user meta value.
1026
+ *
1027
+ * This executes during the `personal_options_update` & `edit_user_profile_update` actions.
1028
+ *
1029
+ * @since 0.1-dev
1030
+ *
1031
+ * @param int $user_id User ID.
1032
+ */
1033
+ public static function user_two_factor_options_update( $user_id ) {
1034
+ if ( isset( $_POST['_nonce_user_two_factor_options'] ) ) {
1035
+ check_admin_referer( 'user_two_factor_options', '_nonce_user_two_factor_options' );
1036
+
1037
+ if ( ! isset( $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ] ) ||
1038
+ ! is_array( $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ] ) ) {
1039
+ return;
1040
+ }
1041
+
1042
+ $providers = self::get_providers();
1043
+
1044
+ $enabled_providers = $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ];
1045
+
1046
+ // Enable only the available providers.
1047
+ $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
1048
+ update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_META_KEY, $enabled_providers );
1049
+
1050
+ // Primary provider must be enabled.
1051
+ $new_provider = isset( $_POST[ self::PROVIDER_USER_META_KEY ] ) ? $_POST[ self::PROVIDER_USER_META_KEY ] : '';
1052
+ if ( ! empty( $new_provider ) && in_array( $new_provider, $enabled_providers, true ) ) {
1053
+ update_user_meta( $user_id, self::PROVIDER_USER_META_KEY, $new_provider );
1054
+ }
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * Should the login session persist between sessions.
1060
+ *
1061
+ * @return boolean
1062
+ */
1063
+ public static function rememberme() {
1064
+ $rememberme = false;
1065
+
1066
+ if ( ! empty( $_REQUEST['rememberme'] ) ) {
1067
+ $rememberme = true;
1068
+ }
1069
+
1070
+ return (bool) apply_filters( 'two_factor_rememberme', $rememberme );
1071
+ }
1072
+ }
includes/Google/u2f-api.js ADDED
@@ -0,0 +1,748 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //Copyright 2014-2015 Google Inc. All rights reserved.
2
+
3
+ //Use of this source code is governed by a BSD-style
4
+ //license that can be found in the LICENSE file or at
5
+ //https://developers.google.com/open-source/licenses/bsd
6
+
7
+ /**
8
+ * @fileoverview The U2F api.
9
+ */
10
+ 'use strict';
11
+
12
+
13
+ /**
14
+ * Namespace for the U2F api.
15
+ * @type {Object}
16
+ */
17
+ var u2f = u2f || {};
18
+
19
+ /**
20
+ * FIDO U2F Javascript API Version
21
+ * @number
22
+ */
23
+ var js_api_version;
24
+
25
+ /**
26
+ * The U2F extension id
27
+ * @const {string}
28
+ */
29
+ // The Chrome packaged app extension ID.
30
+ // Uncomment this if you want to deploy a server instance that uses
31
+ // the package Chrome app and does not require installing the U2F Chrome extension.
32
+ u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
33
+ // The U2F Chrome extension ID.
34
+ // Uncomment this if you want to deploy a server instance that uses
35
+ // the U2F Chrome extension to authenticate.
36
+ // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
37
+
38
+
39
+ /**
40
+ * Message types for messages to/from the extension
41
+ * @const
42
+ * @enum {string}
43
+ */
44
+ u2f.MessageTypes = {
45
+ 'U2F_REGISTER_REQUEST': 'u2f_register_request',
46
+ 'U2F_REGISTER_RESPONSE': 'u2f_register_response',
47
+ 'U2F_SIGN_REQUEST': 'u2f_sign_request',
48
+ 'U2F_SIGN_RESPONSE': 'u2f_sign_response',
49
+ 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
50
+ 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
51
+ };
52
+
53
+
54
+ /**
55
+ * Response status codes
56
+ * @const
57
+ * @enum {number}
58
+ */
59
+ u2f.ErrorCodes = {
60
+ 'OK': 0,
61
+ 'OTHER_ERROR': 1,
62
+ 'BAD_REQUEST': 2,
63
+ 'CONFIGURATION_UNSUPPORTED': 3,
64
+ 'DEVICE_INELIGIBLE': 4,
65
+ 'TIMEOUT': 5
66
+ };
67
+
68
+
69
+ /**
70
+ * A message for registration requests
71
+ * @typedef {{
72
+ * type: u2f.MessageTypes,
73
+ * appId: ?string,
74
+ * timeoutSeconds: ?number,
75
+ * requestId: ?number
76
+ * }}
77
+ */
78
+ u2f.U2fRequest;
79
+
80
+
81
+ /**
82
+ * A message for registration responses
83
+ * @typedef {{
84
+ * type: u2f.MessageTypes,
85
+ * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
86
+ * requestId: ?number
87
+ * }}
88
+ */
89
+ u2f.U2fResponse;
90
+
91
+
92
+ /**
93
+ * An error object for responses
94
+ * @typedef {{
95
+ * errorCode: u2f.ErrorCodes,
96
+ * errorMessage: ?string
97
+ * }}
98
+ */
99
+ u2f.Error;
100
+
101
+ /**
102
+ * Data object for a single sign request.
103
+ * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
104
+ */
105
+ u2f.Transport;
106
+
107
+
108
+ /**
109
+ * Data object for a single sign request.
110
+ * @typedef {Array<u2f.Transport>}
111
+ */
112
+ u2f.Transports;
113
+
114
+ /**
115
+ * Data object for a single sign request.
116
+ * @typedef {{
117
+ * version: string,
118
+ * challenge: string,
119
+ * keyHandle: string,
120
+ * appId: string
121
+ * }}
122
+ */
123
+ u2f.SignRequest;
124
+
125
+
126
+ /**
127
+ * Data object for a sign response.
128
+ * @typedef {{
129
+ * keyHandle: string,
130
+ * signatureData: string,
131
+ * clientData: string
132
+ * }}
133
+ */
134
+ u2f.SignResponse;
135
+
136
+
137
+ /**
138
+ * Data object for a registration request.
139
+ * @typedef {{
140
+ * version: string,
141
+ * challenge: string
142
+ * }}
143
+ */
144
+ u2f.RegisterRequest;
145
+
146
+
147
+ /**
148
+ * Data object for a registration response.
149
+ * @typedef {{
150
+ * version: string,
151
+ * keyHandle: string,
152
+ * transports: Transports,
153
+ * appId: string
154
+ * }}
155
+ */
156
+ u2f.RegisterResponse;
157
+
158
+
159
+ /**
160
+ * Data object for a registered key.
161
+ * @typedef {{
162
+ * version: string,
163
+ * keyHandle: string,
164
+ * transports: ?Transports,
165
+ * appId: ?string
166
+ * }}
167
+ */
168
+ u2f.RegisteredKey;
169
+
170
+
171
+ /**
172
+ * Data object for a get API register response.
173
+ * @typedef {{
174
+ * js_api_version: number
175
+ * }}
176
+ */
177
+ u2f.GetJsApiVersionResponse;
178
+
179
+
180
+ //Low level MessagePort API support
181
+
182
+ /**
183
+ * Sets up a MessagePort to the U2F extension using the
184
+ * available mechanisms.
185
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
186
+ */
187
+ u2f.getMessagePort = function(callback) {
188
+ if (typeof chrome != 'undefined' && chrome.runtime) {
189
+ // The actual message here does not matter, but we need to get a reply
190
+ // for the callback to run. Thus, send an empty signature request
191
+ // in order to get a failure response.
192
+ var msg = {
193
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
194
+ signRequests: []
195
+ };
196
+ chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
197
+ if (!chrome.runtime.lastError) {
198
+ // We are on a whitelisted origin and can talk directly
199
+ // with the extension.
200
+ u2f.getChromeRuntimePort_(callback);
201
+ } else {
202
+ // chrome.runtime was available, but we couldn't message
203
+ // the extension directly, use iframe
204
+ u2f.getIframePort_(callback);
205
+ }
206
+ });
207
+ } else if (u2f.isAndroidChrome_()) {
208
+ u2f.getAuthenticatorPort_(callback);
209
+ } else if (u2f.isIosChrome_()) {
210
+ u2f.getIosPort_(callback);
211
+ } else {
212
+ // chrome.runtime was not available at all, which is normal
213
+ // when this origin doesn't have access to any extensions.
214
+ u2f.getIframePort_(callback);
215
+ }
216
+ };
217
+
218
+ /**
219
+ * Detect chrome running on android based on the browser's useragent.
220
+ * @private
221
+ */
222
+ u2f.isAndroidChrome_ = function() {
223
+ var userAgent = navigator.userAgent;
224
+ return userAgent.indexOf('Chrome') != -1 &&
225
+ userAgent.indexOf('Android') != -1;
226
+ };
227
+
228
+ /**
229
+ * Detect chrome running on iOS based on the browser's platform.
230
+ * @private
231
+ */
232
+ u2f.isIosChrome_ = function() {
233
+ return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1;
234
+ };
235
+
236
+ /**
237
+ * Connects directly to the extension via chrome.runtime.connect.
238
+ * @param {function(u2f.WrappedChromeRuntimePort_)} callback
239
+ * @private
240
+ */
241
+ u2f.getChromeRuntimePort_ = function(callback) {
242
+ var port = chrome.runtime.connect(u2f.EXTENSION_ID,
243
+ {'includeTlsChannelId': true});
244
+ setTimeout(function() {
245
+ callback(new u2f.WrappedChromeRuntimePort_(port));
246
+ }, 0);
247
+ };
248
+
249
+ /**
250
+ * Return a 'port' abstraction to the Authenticator app.
251
+ * @param {function(u2f.WrappedAuthenticatorPort_)} callback
252
+ * @private
253
+ */
254
+ u2f.getAuthenticatorPort_ = function(callback) {
255
+ setTimeout(function() {
256
+ callback(new u2f.WrappedAuthenticatorPort_());
257
+ }, 0);
258
+ };
259
+
260
+ /**
261
+ * Return a 'port' abstraction to the iOS client app.
262
+ * @param {function(u2f.WrappedIosPort_)} callback
263
+ * @private
264
+ */
265
+ u2f.getIosPort_ = function(callback) {
266
+ setTimeout(function() {
267
+ callback(new u2f.WrappedIosPort_());
268
+ }, 0);
269
+ };
270
+
271
+ /**
272
+ * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
273
+ * @param {Port} port
274
+ * @constructor
275
+ * @private
276
+ */
277
+ u2f.WrappedChromeRuntimePort_ = function(port) {
278
+ this.port_ = port;
279
+ };
280
+
281
+ /**
282
+ * Format and return a sign request compliant with the JS API version supported by the extension.
283
+ * @param {Array<u2f.SignRequest>} signRequests
284
+ * @param {number} timeoutSeconds
285
+ * @param {number} reqId
286
+ * @return {Object}
287
+ */
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] = {
295
+ version: registeredKeys[i].version,
296
+ challenge: challenge,
297
+ keyHandle: registeredKeys[i].keyHandle,
298
+ appId: appId
299
+ };
300
+ }
301
+ return {
302
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
303
+ signRequests: signRequests,
304
+ timeoutSeconds: timeoutSeconds,
305
+ requestId: reqId
306
+ };
307
+ }
308
+ // JS 1.1 API.
309
+ return {
310
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
311
+ appId: appId,
312
+ challenge: challenge,
313
+ registeredKeys: registeredKeys,
314
+ timeoutSeconds: timeoutSeconds,
315
+ requestId: reqId
316
+ };
317
+ };
318
+
319
+ /**
320
+ * Format and return a register request compliant with the JS API version supported by the extension..
321
+ * @param {Array<u2f.SignRequest>} signRequests
322
+ * @param {Array<u2f.RegisterRequest>} signRequests
323
+ * @param {number} timeoutSeconds
324
+ * @param {number} reqId
325
+ * @return {Object}
326
+ */
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
+ }
334
+ var signRequests = [];
335
+ for (var i = 0; i < registeredKeys.length; i++) {
336
+ signRequests[i] = {
337
+ version: registeredKeys[i].version,
338
+ challenge: registerRequests[0],
339
+ keyHandle: registeredKeys[i].keyHandle,
340
+ appId: appId
341
+ };
342
+ }
343
+ return {
344
+ type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
345
+ signRequests: signRequests,
346
+ registerRequests: registerRequests,
347
+ timeoutSeconds: timeoutSeconds,
348
+ requestId: reqId
349
+ };
350
+ }
351
+ // JS 1.1 API.
352
+ return {
353
+ type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
354
+ appId: appId,
355
+ registerRequests: registerRequests,
356
+ registeredKeys: registeredKeys,
357
+ timeoutSeconds: timeoutSeconds,
358
+ requestId: reqId
359
+ };
360
+ };
361
+
362
+
363
+ /**
364
+ * Posts a message on the underlying channel.
365
+ * @param {Object} message
366
+ */
367
+ u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
368
+ this.port_.postMessage(message);
369
+ };
370
+
371
+
372
+ /**
373
+ * Emulates the HTML 5 addEventListener interface. Works only for the
374
+ * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
375
+ * @param {string} eventName
376
+ * @param {function({data: Object})} handler
377
+ */
378
+ u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
379
+ function(eventName, handler) {
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 {
387
+ console.error('WrappedChromeRuntimePort only supports onMessage');
388
+ }
389
+ };
390
+
391
+ /**
392
+ * Wrap the Authenticator app with a MessagePort interface.
393
+ * @constructor
394
+ * @private
395
+ */
396
+ u2f.WrappedAuthenticatorPort_ = function() {
397
+ this.requestId_ = -1;
398
+ this.requestObject_ = null;
399
+ }
400
+
401
+ /**
402
+ * Launch the Authenticator intent.
403
+ * @param {Object} message
404
+ */
405
+ u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
406
+ var intentUrl =
407
+ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
408
+ ';S.request=' + encodeURIComponent(JSON.stringify(message)) +
409
+ ';end';
410
+ document.location = intentUrl;
411
+ };
412
+
413
+ /**
414
+ * Tells what type of port this is.
415
+ * @return {String} port type
416
+ */
417
+ u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
418
+ return "WrappedAuthenticatorPort_";
419
+ };
420
+
421
+
422
+ /**
423
+ * Emulates the HTML 5 addEventListener interface.
424
+ * @param {string} eventName
425
+ * @param {function({data: Object})} handler
426
+ */
427
+ u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
428
+ var name = eventName.toLowerCase();
429
+ if (name == 'message') {
430
+ var self = this;
431
+ /* Register a callback to that executes when
432
+ * chrome injects the response. */
433
+ window.addEventListener(
434
+ 'message', self.onRequestUpdate_.bind(self, handler), false);
435
+ } else {
436
+ console.error('WrappedAuthenticatorPort only supports message');
437
+ }
438
+ };
439
+
440
+ /**
441
+ * Callback invoked when a response is received from the Authenticator.
442
+ * @param function({data: Object}) callback
443
+ * @param {Object} message message Object
444
+ */
445
+ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
446
+ function(callback, message) {
447
+ var messageObject = JSON.parse(message.data);
448
+ var intentUrl = messageObject['intentURL'];
449
+
450
+ var errorCode = messageObject['errorCode'];
451
+ var responseObject = null;
452
+ if (messageObject.hasOwnProperty('data')) {
453
+ responseObject = /** @type {Object} */ (
454
+ JSON.parse(messageObject['data']));
455
+ }
456
+
457
+ callback({'data': responseObject});
458
+ };
459
+
460
+ /**
461
+ * Base URL for intents to Authenticator.
462
+ * @const
463
+ * @private
464
+ */
465
+ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
466
+ 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
467
+
468
+ /**
469
+ * Wrap the iOS client app with a MessagePort interface.
470
+ * @constructor
471
+ * @private
472
+ */
473
+ u2f.WrappedIosPort_ = function() {};
474
+
475
+ /**
476
+ * Launch the iOS client app request
477
+ * @param {Object} message
478
+ */
479
+ u2f.WrappedIosPort_.prototype.postMessage = function(message) {
480
+ var str = JSON.stringify(message);
481
+ var url = "u2f://auth?" + encodeURI(str);
482
+ location.replace(url);
483
+ };
484
+
485
+ /**
486
+ * Tells what type of port this is.
487
+ * @return {String} port type
488
+ */
489
+ u2f.WrappedIosPort_.prototype.getPortType = function() {
490
+ return "WrappedIosPort_";
491
+ };
492
+
493
+ /**
494
+ * Emulates the HTML 5 addEventListener interface.
495
+ * @param {string} eventName
496
+ * @param {function({data: Object})} handler
497
+ */
498
+ u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
499
+ var name = eventName.toLowerCase();
500
+ if (name !== 'message') {
501
+ console.error('WrappedIosPort only supports message');
502
+ }
503
+ };
504
+
505
+ /**
506
+ * Sets up an embedded trampoline iframe, sourced from the extension.
507
+ * @param {function(MessagePort)} callback
508
+ * @private
509
+ */
510
+ u2f.getIframePort_ = function(callback) {
511
+ // Create the iframe
512
+ var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
513
+ var iframe = document.createElement('iframe');
514
+ iframe.src = iframeOrigin + '/u2f-comms.html';
515
+ iframe.setAttribute('style', 'display:none');
516
+ document.body.appendChild(iframe);
517
+
518
+ var channel = new MessageChannel();
519
+ var ready = function(message) {
520
+ if (message.data == 'ready') {
521
+ channel.port1.removeEventListener('message', ready);
522
+ callback(channel.port1);
523
+ } else {
524
+ console.error('First event on iframe port was not "ready"');
525
+ }
526
+ };
527
+ channel.port1.addEventListener('message', ready);
528
+ channel.port1.start();
529
+
530
+ iframe.addEventListener('load', function() {
531
+ // Deliver the port to the iframe and initialize
532
+ iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
533
+ });
534
+ };
535
+
536
+
537
+ //High-level JS API
538
+
539
+ /**
540
+ * Default extension response timeout in seconds.
541
+ * @const
542
+ */
543
+ u2f.EXTENSION_TIMEOUT_SEC = 30;
544
+
545
+ /**
546
+ * A singleton instance for a MessagePort to the extension.
547
+ * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
548
+ * @private
549
+ */
550
+ u2f.port_ = null;
551
+
552
+ /**
553
+ * Callbacks waiting for a port
554
+ * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
555
+ * @private
556
+ */
557
+ u2f.waitingForPort_ = [];
558
+
559
+ /**
560
+ * A counter for requestIds.
561
+ * @type {number}
562
+ * @private
563
+ */
564
+ u2f.reqCounter_ = 0;
565
+
566
+ /**
567
+ * A map from requestIds to client callbacks
568
+ * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
569
+ * |function((u2f.Error|u2f.SignResponse)))>}
570
+ * @private
571
+ */
572
+ u2f.callbackMap_ = {};
573
+
574
+ /**
575
+ * Creates or retrieves the MessagePort singleton to use.
576
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
577
+ * @private
578
+ */
579
+ u2f.getPortSingleton_ = function(callback) {
580
+ if (u2f.port_) {
581
+ callback(u2f.port_);
582
+ } else {
583
+ if (u2f.waitingForPort_.length == 0) {
584
+ u2f.getMessagePort(function(port) {
585
+ u2f.port_ = port;
586
+ u2f.port_.addEventListener('message',
587
+ /** @type {function(Event)} */ (u2f.responseHandler_));
588
+
589
+ // Careful, here be async callbacks. Maybe.
590
+ while (u2f.waitingForPort_.length)
591
+ u2f.waitingForPort_.shift()(u2f.port_);
592
+ });
593
+ }
594
+ u2f.waitingForPort_.push(callback);
595
+ }
596
+ };
597
+
598
+ /**
599
+ * Handles response messages from the extension.
600
+ * @param {MessageEvent.<u2f.Response>} message
601
+ * @private
602
+ */
603
+ u2f.responseHandler_ = function(message) {
604
+ var response = message.data;
605
+ var reqId = response['requestId'];
606
+ if (!reqId || !u2f.callbackMap_[reqId]) {
607
+ console.error('Unknown or missing requestId in response.');
608
+ return;
609
+ }
610
+ var cb = u2f.callbackMap_[reqId];
611
+ delete u2f.callbackMap_[reqId];
612
+ cb(response['responseData']);
613
+ };
614
+
615
+ /**
616
+ * Dispatches an array of sign requests to available U2F tokens.
617
+ * If the JS API version supported by the extension is unknown, it first sends a
618
+ * message to the extension to find out the supported API version and then it sends
619
+ * the sign request.
620
+ * @param {string=} appId
621
+ * @param {string=} challenge
622
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
623
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
624
+ * @param {number=} opt_timeoutSeconds
625
+ */
626
+ u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
627
+ if (js_api_version === undefined) {
628
+ // Send a message to get the extension to JS API version, then send the actual sign request.
629
+ u2f.getApiVersion(
630
+ function (response) {
631
+ js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
632
+ console.log("Extension JS API Version: ", js_api_version);
633
+ u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
634
+ });
635
+ } else {
636
+ // We know the JS API version. Send the actual sign request in the supported API version.
637
+ u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
638
+ }
639
+ };
640
+
641
+ /**
642
+ * Dispatches an array of sign requests to available U2F tokens.
643
+ * @param {string=} appId
644
+ * @param {string=} challenge
645
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
646
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
647
+ * @param {number=} opt_timeoutSeconds
648
+ */
649
+ u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
650
+ u2f.getPortSingleton_(function(port) {
651
+ var reqId = ++u2f.reqCounter_;
652
+ u2f.callbackMap_[reqId] = callback;
653
+ var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
654
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
655
+ var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
656
+ port.postMessage(req);
657
+ });
658
+ };
659
+
660
+ /**
661
+ * Dispatches register requests to available U2F tokens. An array of sign
662
+ * requests identifies already registered tokens.
663
+ * If the JS API version supported by the extension is unknown, it first sends a
664
+ * message to the extension to find out the supported API version and then it sends
665
+ * the register request.
666
+ * @param {string=} appId
667
+ * @param {Array<u2f.RegisterRequest>} registerRequests
668
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
669
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
670
+ * @param {number=} opt_timeoutSeconds
671
+ */
672
+ u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
673
+ if (js_api_version === undefined) {
674
+ // Send a message to get the extension to JS API version, then send the actual register request.
675
+ u2f.getApiVersion(
676
+ function (response) {
677
+ js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
678
+ console.log("Extension JS API Version: ", js_api_version);
679
+ u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
680
+ callback, opt_timeoutSeconds);
681
+ });
682
+ } else {
683
+ // We know the JS API version. Send the actual register request in the supported API version.
684
+ u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
685
+ callback, opt_timeoutSeconds);
686
+ }
687
+ };
688
+
689
+ /**
690
+ * Dispatches register requests to available U2F tokens. An array of sign
691
+ * requests identifies already registered tokens.
692
+ * @param {string=} appId
693
+ * @param {Array<u2f.RegisterRequest>} registerRequests
694
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
695
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
696
+ * @param {number=} opt_timeoutSeconds
697
+ */
698
+ u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
699
+ u2f.getPortSingleton_(function(port) {
700
+ var reqId = ++u2f.reqCounter_;
701
+ u2f.callbackMap_[reqId] = callback;
702
+ var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
703
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
704
+ var req = u2f.formatRegisterRequest_(
705
+ appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
706
+ port.postMessage(req);
707
+ });
708
+ };
709
+
710
+
711
+ /**
712
+ * Dispatches a message to the extension to find out the supported
713
+ * JS API version.
714
+ * If the user is on a mobile phone and is thus using Google Authenticator instead
715
+ * of the Chrome extension, don't send the request and simply return 0.
716
+ * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
717
+ * @param {number=} opt_timeoutSeconds
718
+ */
719
+ u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
720
+ u2f.getPortSingleton_(function(port) {
721
+ // If we are using Android Google Authenticator or iOS client app,
722
+ // do not fire an intent to ask which JS API version to use.
723
+ if (port.getPortType) {
724
+ var apiVersion;
725
+ switch (port.getPortType()) {
726
+ case 'WrappedIosPort_':
727
+ case 'WrappedAuthenticatorPort_':
728
+ apiVersion = 1.1;
729
+ break;
730
+
731
+ default:
732
+ apiVersion = 0;
733
+ break;
734
+ }
735
+ callback({ 'js_api_version': apiVersion });
736
+ return;
737
+ }
738
+ var reqId = ++u2f.reqCounter_;
739
+ u2f.callbackMap_[reqId] = callback;
740
+ var req = {
741
+ type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
742
+ timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
743
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
744
+ requestId: reqId
745
+ };
746
+ port.postMessage(req);
747
+ });
748
+ };
includes/Yubico/U2F.php ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /* Copyright (c) 2014 Yubico AB
3
+ * All rights reserved.
4
+ *
5
+ * Redistribution and use in source and binary forms, with or without
6
+ * modification, are permitted provided that the following conditions are
7
+ * met:
8
+ *
9
+ * * Redistributions of source code must retain the above copyright
10
+ * notice, this list of conditions and the following disclaimer.
11
+ *
12
+ * * Redistributions in binary form must reproduce the above
13
+ * copyright notice, this list of conditions and the following
14
+ * disclaimer in the documentation and/or other materials provided
15
+ * with the distribution.
16
+ *
17
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+ */
29
+
30
+ namespace u2flib_server;
31
+
32
+ /** Constant for the version of the u2f protocol */
33
+ const U2F_VERSION = "U2F_V2";
34
+
35
+ /** Error for the authentication message not matching any outstanding
36
+ * authentication request */
37
+ const ERR_NO_MATCHING_REQUEST = 1;
38
+
39
+ /** Error for the authentication message not matching any registration */
40
+ const ERR_NO_MATCHING_REGISTRATION = 2;
41
+
42
+ /** Error for the signature on the authentication message not verifying with
43
+ * the correct key */
44
+ const ERR_AUTHENTICATION_FAILURE = 3;
45
+
46
+ /** Error for the challenge in the registration message not matching the
47
+ * registration challenge */
48
+ const ERR_UNMATCHED_CHALLENGE = 4;
49
+
50
+ /** Error for the attestation signature on the registration message not
51
+ * verifying */
52
+ const ERR_ATTESTATION_SIGNATURE = 5;
53
+
54
+ /** Error for the attestation verification not verifying */
55
+ const ERR_ATTESTATION_VERIFICATION = 6;
56
+
57
+ /** Error for not getting good random from the system */
58
+ const ERR_BAD_RANDOM = 7;
59
+
60
+ /** Error when the counter is lower than expected */
61
+ const ERR_COUNTER_TOO_LOW = 8;
62
+
63
+ /** Error decoding public key */
64
+ const ERR_PUBKEY_DECODE = 9;
65
+
66
+ /** Error user-agent returned error */
67
+ const ERR_BAD_UA_RETURNING = 10;
68
+
69
+ /** Error old OpenSSL version */
70
+ const ERR_OLD_OPENSSL = 11;
71
+
72
+ /** @internal */
73
+ const PUBKEY_LEN = 65;
74
+
75
+ class U2F
76
+ {
77
+ /** @var string */
78
+ private $appId;
79
+
80
+ /** @var null|string */
81
+ private $attestDir;
82
+
83
+ /** @internal */
84
+ private $FIXCERTS = array(
85
+ '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
86
+ 'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
87
+ '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
88
+ 'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
89
+ '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
90
+ 'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
91
+ );
92
+
93
+ /**
94
+ * @param string $appId Application id for the running application
95
+ * @param string|null $attestDir Directory where trusted attestation roots may be found
96
+ * @throws Error If OpenSSL older than 1.0.0 is used
97
+ */
98
+ public function __construct($appId, $attestDir = null)
99
+ {
100
+ if(OPENSSL_VERSION_NUMBER < 0x10000000) {
101
+ throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL);
102
+ }
103
+ $this->appId = $appId;
104
+ $this->attestDir = $attestDir;
105
+ }
106
+
107
+ /**
108
+ * Called to get a registration request to send to a user.
109
+ * Returns an array of one registration request and a array of sign requests.
110
+ *
111
+ * @param array $registrations List of current registrations for this
112
+ * user, to prevent the user from registering the same authenticator several
113
+ * times.
114
+ * @return array An array of two elements, the first containing a
115
+ * RegisterRequest the second being an array of SignRequest
116
+ * @throws Error
117
+ */
118
+ public function getRegisterData(array $registrations = array())
119
+ {
120
+ $challenge = $this->createChallenge();
121
+ $request = new RegisterRequest($challenge, $this->appId);
122
+ $signs = $this->getAuthenticateData($registrations);
123
+ return array($request, $signs);
124
+ }
125
+
126
+ /**
127
+ * Called to verify and unpack a registration message.
128
+ *
129
+ * @param RegisterRequest $request this is a reply to
130
+ * @param object $response response from a user
131
+ * @param bool $includeCert set to true if the attestation certificate should be
132
+ * included in the returned Registration object
133
+ * @return Registration
134
+ * @throws Error
135
+ */
136
+ public function doRegister($request, $response, $includeCert = true)
137
+ {
138
+ if( !is_object( $request ) ) {
139
+ throw new \InvalidArgumentException('$request of doRegister() method only accepts object.');
140
+ }
141
+
142
+ if( !is_object( $response ) ) {
143
+ throw new \InvalidArgumentException('$response of doRegister() method only accepts object.');
144
+ }
145
+
146
+ if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
147
+ throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
148
+ }
149
+
150
+ if( !is_bool( $includeCert ) ) {
151
+ throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.');
152
+ }
153
+
154
+ $rawReg = $this->base64u_decode($response->registrationData);
155
+ $regData = array_values(unpack('C*', $rawReg));
156
+ $clientData = $this->base64u_decode($response->clientData);
157
+ $cli = json_decode($clientData);
158
+
159
+ if($cli->challenge !== $request->challenge) {
160
+ throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE );
161
+ }
162
+
163
+ $registration = new Registration();
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 );
171
+ }
172
+ $registration->publicKey = base64_encode($pubKey);
173
+ $khLen = $regData[$offs++];
174
+ $kh = substr($rawReg, $offs, $khLen);
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];
182
+
183
+ $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen));
184
+ $offs += $certLen;
185
+ $pemCert = "-----BEGIN CERTIFICATE-----\r\n";
186
+ $pemCert .= chunk_split(base64_encode($rawCert), 64);
187
+ $pemCert .= "-----END CERTIFICATE-----";
188
+ if($includeCert) {
189
+ $registration->certificate = base64_encode($rawCert);
190
+ }
191
+ if($this->attestDir) {
192
+ if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) {
193
+ throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION );
194
+ }
195
+ }
196
+
197
+ if(!openssl_pkey_get_public($pemCert)) {
198
+ throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
199
+ }
200
+ $signature = substr($rawReg, $offs);
201
+
202
+ $dataToVerify = chr(0);
203
+ $dataToVerify .= hash('sha256', $request->appId, true);
204
+ $dataToVerify .= hash('sha256', $clientData, true);
205
+ $dataToVerify .= $kh;
206
+ $dataToVerify .= $pubKey;
207
+
208
+ if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) {
209
+ return $registration;
210
+ } else {
211
+ throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE );
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Called to get an authentication request.
217
+ *
218
+ * @param array $registrations An array of the registrations to create authentication requests for.
219
+ * @return array An array of SignRequest
220
+ * @throws Error
221
+ */
222
+ public function getAuthenticateData(array $registrations)
223
+ {
224
+ $sigs = array();
225
+ $challenge = $this->createChallenge();
226
+ foreach ($registrations as $reg) {
227
+ if( !is_object( $reg ) ) {
228
+ throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.');
229
+ }
230
+
231
+ $sig = new SignRequest();
232
+ $sig->appId = $this->appId;
233
+ $sig->keyHandle = $reg->keyHandle;
234
+ $sig->challenge = $challenge;
235
+ $sigs[] = $sig;
236
+ }
237
+ return $sigs;
238
+ }
239
+
240
+ /**
241
+ * Called to verify an authentication response
242
+ *
243
+ * @param array $requests An array of outstanding authentication requests
244
+ * @param array $registrations An array of current registrations
245
+ * @param object $response A response from the authenticator
246
+ * @return Registration
247
+ * @throws Error
248
+ *
249
+ * The Registration object returned on success contains an updated counter
250
+ * that should be saved for future authentications.
251
+ * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of
252
+ * token cloning or similar and appropriate action should be taken.
253
+ */
254
+ public function doAuthenticate(array $requests, array $registrations, $response)
255
+ {
256
+ if( !is_object( $response ) ) {
257
+ throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.');
258
+ }
259
+
260
+ if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
261
+ throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
262
+ }
263
+
264
+ /** @var object|null $req */
265
+ $req = null;
266
+
267
+ /** @var object|null $reg */
268
+ $reg = null;
269
+
270
+ $clientData = $this->base64u_decode($response->clientData);
271
+ $decodedClient = json_decode($clientData);
272
+ foreach ($requests as $req) {
273
+ if( !is_object( $req ) ) {
274
+ throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.');
275
+ }
276
+
277
+ if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) {
278
+ break;
279
+ }
280
+
281
+ $req = null;
282
+ }
283
+ if($req === null) {
284
+ throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST );
285
+ }
286
+ foreach ($registrations as $reg) {
287
+ if( !is_object( $reg ) ) {
288
+ throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.');
289
+ }
290
+
291
+ if($reg->keyHandle === $response->keyHandle) {
292
+ break;
293
+ }
294
+ $reg = null;
295
+ }
296
+ if($reg === null) {
297
+ throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION );
298
+ }
299
+ $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey));
300
+ if($pemKey === null) {
301
+ throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
302
+ }
303
+
304
+ $signData = $this->base64u_decode($response->signatureData);
305
+ $dataToVerify = hash('sha256', $req->appId, true);
306
+ $dataToVerify .= substr($signData, 0, 5);
307
+ $dataToVerify .= hash('sha256', $clientData, true);
308
+ $signature = substr($signData, 5);
309
+
310
+ if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) {
311
+ $ctr = unpack("Nctr", substr($signData, 1, 4));
312
+ $counter = $ctr['ctr'];
313
+ /* TODO: wrap-around should be handled somehow.. */
314
+ if($counter > $reg->counter) {
315
+ $reg->counter = $counter;
316
+ return $reg;
317
+ } else {
318
+ throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW );
319
+ }
320
+ } else {
321
+ throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE );
322
+ }
323
+ }
324
+
325
+ /**
326
+ * @return array
327
+ */
328
+ private function get_certs()
329
+ {
330
+ $files = array();
331
+ $dir = $this->attestDir;
332
+ if($dir && $handle = opendir($dir)) {
333
+ while(false !== ($entry = readdir($handle))) {
334
+ if(is_file("$dir/$entry")) {
335
+ $files[] = "$dir/$entry";
336
+ }
337
+ }
338
+ closedir($handle);
339
+ }
340
+ return $files;
341
+ }
342
+
343
+ /**
344
+ * @param string $data
345
+ * @return string
346
+ */
347
+ private function base64u_encode($data)
348
+ {
349
+ return trim(strtr(base64_encode($data), '+/', '-_'), '=');
350
+ }
351
+
352
+ /**
353
+ * @param string $data
354
+ * @return string
355
+ */
356
+ private function base64u_decode($data)
357
+ {
358
+ return base64_decode(strtr($data, '-_', '+/'));
359
+ }
360
+
361
+ /**
362
+ * @param string $key
363
+ * @return null|string
364
+ */
365
+ private function pubkey_to_pem($key)
366
+ {
367
+ if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") {
368
+ return null;
369
+ }
370
+
371
+ /*
372
+ * Convert the public key to binary DER format first
373
+ * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480
374
+ *
375
+ * SEQUENCE(2 elem) 30 59
376
+ * SEQUENCE(2 elem) 30 13
377
+ * OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01
378
+ * OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07
379
+ * BIT STRING(520 bit) 03 42 ..key..
380
+ */
381
+ $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
382
+ $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
383
+ $der .= "\0".$key;
384
+
385
+ $pem = "-----BEGIN PUBLIC KEY-----\r\n";
386
+ $pem .= chunk_split(base64_encode($der), 64);
387
+ $pem .= "-----END PUBLIC KEY-----";
388
+
389
+ return $pem;
390
+ }
391
+
392
+ /**
393
+ * @return string
394
+ * @throws Error
395
+ */
396
+ private function createChallenge()
397
+ {
398
+ $challenge = openssl_random_pseudo_bytes(32, $crypto_strong );
399
+ if( $crypto_strong !== true ) {
400
+ throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM);
401
+ }
402
+
403
+ $challenge = $this->base64u_encode( $challenge );
404
+
405
+ return $challenge;
406
+ }
407
+
408
+ /**
409
+ * Fixes a certificate where the signature contains unused bits.
410
+ *
411
+ * @param string $cert
412
+ * @return mixed
413
+ */
414
+ private function fixSignatureUnusedBits($cert)
415
+ {
416
+ if(in_array(hash('sha256', $cert), $this->FIXCERTS)) {
417
+ $cert[strlen($cert) - 257] = "\0";
418
+ }
419
+ return $cert;
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Class for building a registration request
425
+ *
426
+ * @package u2flib_server
427
+ */
428
+ class RegisterRequest
429
+ {
430
+ /** Protocol version */
431
+ public $version = U2F_VERSION;
432
+
433
+ /** Registration challenge */
434
+ public $challenge;
435
+
436
+ /** Application id */
437
+ public $appId;
438
+
439
+ /**
440
+ * @param string $challenge
441
+ * @param string $appId
442
+ * @internal
443
+ */
444
+ public function __construct($challenge, $appId)
445
+ {
446
+ $this->challenge = $challenge;
447
+ $this->appId = $appId;
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Class for building up an authentication request
453
+ *
454
+ * @package u2flib_server
455
+ */
456
+ class SignRequest
457
+ {
458
+ /** Protocol version */
459
+ public $version = U2F_VERSION;
460
+
461
+ /** Authentication challenge */
462
+ public $challenge;
463
+
464
+ /** Key handle of a registered authenticator */
465
+ public $keyHandle;
466
+
467
+ /** Application id */
468
+ public $appId;
469
+ }
470
+
471
+ /**
472
+ * Class returned for successful registrations
473
+ *
474
+ * @package u2flib_server
475
+ */
476
+ class Registration
477
+ {
478
+ /** The key handle of the registered authenticator */
479
+ public $keyHandle;
480
+
481
+ /** The public key of the registered authenticator */
482
+ public $publicKey;
483
+
484
+ /** The attestation certificate of the registered authenticator */
485
+ public $certificate;
486
+
487
+ /** The counter associated with this registration */
488
+ public $counter = -1;
489
+ }
490
+
491
+ /**
492
+ * Error class, returned on errors
493
+ *
494
+ * @package u2flib_server
495
+ */
496
+ class Error extends \Exception
497
+ {
498
+ /**
499
+ * Override constructor and make message and code mandatory
500
+ * @param string $message
501
+ * @param int $code
502
+ * @param \Exception|null $previous
503
+ */
504
+ public function __construct($message, $code, \Exception $previous = null) {
505
+ parent::__construct($message, $code, $previous);
506
+ }
507
+ }
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 ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Extracted from wp-login.php since that file also loads WP core which we already have.
4
+ */
5
+
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.
20
+ * @param WP_Error $wp_error Optional. The error to pass. Default is a WP_Error instance.
21
+ */
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
+
31
+ if ( ! is_wp_error( $wp_error ) ) {
32
+ $wp_error = new WP_Error();
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
+ *
40
+ * @since 3.0.0
41
+ *
42
+ * @param array $shake_error_codes Error codes that shake the login form.
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
+ *
63
+ * @since 4.9.0
64
+ *
65
+ * @param string $login_title The page title, with extra context added.
66
+ * @param string $title The original page title.
67
+ */
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
+
77
+ wp_enqueue_style( 'login' );
78
+
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
88
+ }
89
+
90
+ /**
91
+ * Enqueue scripts and styles for the login page.
92
+ *
93
+ * @since 3.1.0
94
+ */
95
+ do_action( 'login_enqueue_scripts' );
96
+
97
+ /**
98
+ * Fires in the login page header after scripts are enqueued.
99
+ *
100
+ * @since 2.1.0
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.
108
+ *
109
+ * @since 2.1.0
110
+ *
111
+ * @param string $login_header_url Login header logo URL.
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.
166
+ *
167
+ * @since 3.5.0
168
+ *
169
+ * @param array $classes An array of body classes.
170
+ * @param string $action The action that brought the visitor to the login page.
171
+ */
172
+ $classes = apply_filters( 'login_body_class', $classes, $action );
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.
183
+ *
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
+ *
195
+ * @since 2.1.0
196
+ *
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.
229
+ *
230
+ * @since 2.1.0
231
+ *
232
+ * @param string $errors Login error message.
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.
240
+ *
241
+ * @since 2.5.0
242
+ *
243
+ * @param string $messages Login messages.
244
+ */
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" />
258
+ <?php
259
+ }
providers/class-two-factor-backup-codes.php ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ *
11
+ * @since 0.1-dev
12
+ *
13
+ * @package Two_Factor
14
+ */
15
+ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
16
+
17
+ /**
18
+ * The user meta backup codes key.
19
+ *
20
+ * @type string
21
+ */
22
+ const BACKUP_CODES_META_KEY = '_two_factor_backup_codes';
23
+
24
+ /**
25
+ * The number backup codes.
26
+ *
27
+ * @type int
28
+ */
29
+ const NUMBER_OF_CODES = 10;
30
+
31
+ /**
32
+ * Ensures only one instance of this class exists in memory at any one time.
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
+ }
44
+
45
+ /**
46
+ * Class constructor.
47
+ *
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
+
55
+ return parent::__construct();
56
+ }
57
+
58
+ /**
59
+ * Displays an admin notice when backup codes have run out.
60
+ *
61
+ * @since 0.1-dev
62
+ */
63
+ public function admin_notices() {
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
+
71
+ // Return if we are not out of codes.
72
+ if ( $this->is_available_for_user( $user ) ) {
73
+ return;
74
+ }
75
+ ?>
76
+ <div class="error">
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>
90
+ </p>
91
+ </div>
92
+ <?php
93
+ }
94
+
95
+ /**
96
+ * Returns the name of the provider.
97
+ *
98
+ * @since 0.1-dev
99
+ */
100
+ public function get_label() {
101
+ return _x( 'Backup Verification Codes (Single Use)', 'Provider Label', 'two-factor' );
102
+ }
103
+
104
+ /**
105
+ * Whether this Two Factor provider is configured and codes are available for the user specified.
106
+ *
107
+ * @since 0.1-dev
108
+ *
109
+ * @param WP_User $user WP_User object of the logged-in user.
110
+ * @return boolean
111
+ */
112
+ public function is_available_for_user( $user ) {
113
+ // Does this user have available codes?
114
+ if ( 0 < self::codes_remaining_for_user( $user ) ) {
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+
120
+ /**
121
+ * Inserts markup at the end of the user profile field for this provider.
122
+ *
123
+ * @since 0.1-dev
124
+ *
125
+ * @param WP_User $user WP_User object of the logged-in user.
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>
149
+ <p class="description"><?php esc_html_e( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'two-factor' ); ?></p>
150
+ <p>
151
+ <a class="button button-two-factor-backup-codes-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'two-factor' ); ?></a>
152
+ <p>
153
+ </div>
154
+ <script type="text/javascript">
155
+ ( function( $ ) {
156
+ $( '.button-two-factor-backup-codes-generate' ).click( function() {
157
+ $.ajax( {
158
+ method: 'POST',
159
+ url: ajaxurl,
160
+ data: {
161
+ action: 'two_factor_backup_codes_generate',
162
+ user_id: '<?php echo esc_js( $user->ID ); ?>',
163
+ nonce: '<?php echo esc_js( $ajax_nonce ); ?>'
164
+ },
165
+ dataType: 'JSON',
166
+ success: function( response ) {
167
+ var $codesList = $( '.two-factor-backup-codes-unused-codes' );
168
+
169
+ $( '.two-factor-backup-codes-wrapper' ).show();
170
+ $codesList.html( '' );
171
+
172
+ // Append the codes.
173
+ for ( i = 0; i < response.data.codes.length; i++ ) {
174
+ $codesList.append( '<li>' + response.data.codes[ i ] + '</li>' );
175
+ }
176
+
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
+
184
+ for ( i = 0; i < response.data.codes.length; i++ ) {
185
+ txt_data += i + 1 + '. ' + response.data.codes[ i ] + '\n';
186
+ }
187
+
188
+ $( '#two-factor-backup-codes-download-link' ).attr( 'href', encodeURI( txt_data ) );
189
+ }
190
+ } );
191
+ } );
192
+ } )( jQuery );
193
+ </script>
194
+ <?php
195
+ }
196
+
197
+ /**
198
+ * Generates backup codes & updates the user meta.
199
+ *
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.
211
+ if ( isset( $args['number'] ) ) {
212
+ $num_codes = (int) $args['number'];
213
+ } else {
214
+ $num_codes = self::NUMBER_OF_CODES;
215
+ }
216
+
217
+ // Append or replace (default).
218
+ if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
219
+ $codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
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
+
229
+ update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed );
230
+
231
+ // Unhashed.
232
+ return $codes;
233
+ }
234
+
235
+ /**
236
+ * Generates a JSON object of backup codes.
237
+ *
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 */
251
+ 'title' => esc_html__( 'Two-Factor Backup Codes for %s', 'two-factor' ),
252
+ );
253
+
254
+ // Send the response.
255
+ wp_send_json_success(
256
+ array(
257
+ 'codes' => $codes,
258
+ 'i18n' => $i18n,
259
+ )
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Returns the number of unused codes for the specified user
265
+ *
266
+ * @param WP_User $user WP_User object of the logged-in user.
267
+ * @return int $int The number of unused codes remaining
268
+ */
269
+ public static function codes_remaining_for_user( $user ) {
270
+ $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
271
+ if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
272
+ return count( $backup_codes );
273
+ }
274
+ return 0;
275
+ }
276
+
277
+ /**
278
+ * Prints the form that prompts the user to authenticate.
279
+ *
280
+ * @since 0.1-dev
281
+ *
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>
289
+ <label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
290
+ <input type="tel" name="two-factor-backup-code" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
291
+ </p>
292
+ <?php
293
+ submit_button( __( 'Submit', 'two-factor' ) );
294
+ }
295
+
296
+ /**
297
+ * Validates the users input token.
298
+ *
299
+ * In this class we just return true.
300
+ *
301
+ * @since 0.1-dev
302
+ *
303
+ * @param WP_User $user WP_User object of the logged-in user.
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'] ) ) : false;
308
+ return $this->validate_code( $user, filter_var( $backup_code, FILTER_SANITIZE_STRING ) );
309
+ }
310
+
311
+ /**
312
+ * Validates a backup code.
313
+ *
314
+ * Backup Codes are single use and are deleted upon a successful validation.
315
+ *
316
+ * @since 0.1-dev
317
+ *
318
+ * @param WP_User $user WP_User object of the logged-in user.
319
+ * @param int $code The backup code.
320
+ * @return boolean
321
+ */
322
+ public function validate_code( $user, $code ) {
323
+ $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
324
+
325
+ if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
326
+ foreach ( $backup_codes as $code_index => $code_hashed ) {
327
+ if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
328
+ $this->delete_code( $user, $code_hashed );
329
+ return true;
330
+ }
331
+ }
332
+ }
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * Deletes a backup code.
338
+ *
339
+ * @since 0.1-dev
340
+ *
341
+ * @param WP_User $user WP_User object of the logged-in user.
342
+ * @param string $code_hashed The hashed the backup code.
343
+ */
344
+ public function delete_code( $user, $code_hashed ) {
345
+ $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
346
+
347
+ // Delete the current code from the list since it's been used.
348
+ $backup_codes = array_flip( $backup_codes );
349
+ unset( $backup_codes[ $code_hashed ] );
350
+ $backup_codes = array_values( array_flip( $backup_codes ) );
351
+
352
+ // Update the backup code master list.
353
+ update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes );
354
+ }
355
+ }
providers/class-two-factor-dummy.php ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ *
11
+ * @since 0.1-dev
12
+ *
13
+ * @package Two_Factor
14
+ */
15
+ class Two_Factor_Dummy extends Two_Factor_Provider {
16
+
17
+ /**
18
+ * Ensures only one instance of this class exists in memory at any one time.
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
+ }
30
+
31
+ /**
32
+ * Class constructor.
33
+ *
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
+
41
+ /**
42
+ * Returns the name of the provider.
43
+ *
44
+ * @since 0.1-dev
45
+ */
46
+ public function get_label() {
47
+ return _x( 'Dummy Method', 'Provider Label', 'two-factor' );
48
+ }
49
+
50
+ /**
51
+ * Prints the form that prompts the user to authenticate.
52
+ *
53
+ * @since 0.1-dev
54
+ *
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
62
+ submit_button( __( 'Yup.', 'two-factor' ) );
63
+ }
64
+
65
+ /**
66
+ * Validates the users input token.
67
+ *
68
+ * In this class we just return true.
69
+ *
70
+ * @since 0.1-dev
71
+ *
72
+ * @param WP_User $user WP_User object of the logged-in user.
73
+ * @return boolean
74
+ */
75
+ public function validate_authentication( $user ) {
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Whether this Two Factor provider is configured and available for the user specified.
81
+ *
82
+ * @since 0.1-dev
83
+ *
84
+ * @param WP_User $user WP_User object of the logged-in user.
85
+ * @return boolean
86
+ */
87
+ public function is_available_for_user( $user ) {
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Inserts markup at the end of the user profile field for this provider.
93
+ *
94
+ * @since 0.1-dev
95
+ *
96
+ * @param WP_User $user WP_User object of the logged-in user.
97
+ */
98
+ public function user_options( $user ) {}
99
+ }
providers/class-two-factor-email.php ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ *
11
+ * @since 0.1-dev
12
+ *
13
+ * @package Two_Factor
14
+ */
15
+ class Two_Factor_Email extends Two_Factor_Provider {
16
+
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
+ *
34
+ * @var string
35
+ */
36
+ const INPUT_NAME_RESEND_CODE = 'two-factor-email-code-resend';
37
+
38
+ /**
39
+ * Ensures only one instance of this class exists in memory at any one time.
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
+ }
51
+
52
+ /**
53
+ * Class constructor.
54
+ *
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
+
62
+ /**
63
+ * Returns the name of the provider.
64
+ *
65
+ * @since 0.1-dev
66
+ */
67
+ public function get_label() {
68
+ return _x( 'Email', 'Provider Label', 'two-factor' );
69
+ }
70
+
71
+ /**
72
+ * Generate the user token.
73
+ *
74
+ * @since 0.1-dev
75
+ *
76
+ * @param int $user_id User ID.
77
+ * @return string
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
+
88
+ /**
89
+ * Check if user has a valid token already.
90
+ *
91
+ * @param int $user_id User ID.
92
+ * @return boolean If user has a valid email token.
93
+ */
94
+ public function user_has_token( $user_id ) {
95
+ $hashed_token = $this->get_user_token( $user_id );
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
+ /**
161
+ * Get the authentication token for the user.
162
+ *
163
+ * @param int $user_id User ID.
164
+ *
165
+ * @return string|boolean User token or `false` if no token found.
166
+ */
167
+ public function get_user_token( $user_id ) {
168
+ $hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
169
+
170
+ if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
171
+ return $hashed_token;
172
+ }
173
+
174
+ return false;
175
+ }
176
+
177
+ /**
178
+ * Validate the user token.
179
+ *
180
+ * @since 0.1-dev
181
+ *
182
+ * @param int $user_id User ID.
183
+ * @param string $token User token.
184
+ * @return boolean
185
+ */
186
+ public function validate_token( $user_id, $token ) {
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;
202
+ }
203
+
204
+ /**
205
+ * Delete the user token.
206
+ *
207
+ * @since 0.1-dev
208
+ *
209
+ * @param int $user_id User ID.
210
+ */
211
+ public function delete_token( $user_id ) {
212
+ delete_user_meta( $user_id, self::TOKEN_META_KEY );
213
+ }
214
+
215
+ /**
216
+ * Generate and email the user token.
217
+ *
218
+ * @since 0.1-dev
219
+ *
220
+ * @param WP_User $user WP_User object of the logged-in user.
221
+ * @return bool Whether the email contents were sent successfully.
222
+ */
223
+ public function generate_and_email_token( $user ) {
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
+ /**
252
+ * Prints the form that prompts the user to authenticate.
253
+ *
254
+ * @since 0.1-dev
255
+ *
256
+ * @param WP_User $user WP_User object of the logged-in user.
257
+ */
258
+ public function authentication_page( $user ) {
259
+ if ( ! $user ) {
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">
276
+ <input type="submit" class="button" name="<?php echo esc_attr( self::INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'two-factor' ); ?>" />
277
+ </p>
278
+ <script type="text/javascript">
279
+ setTimeout( function(){
280
+ var d;
281
+ try{
282
+ d = document.getElementById('authcode');
283
+ d.value = '';
284
+ d.focus();
285
+ } catch(e){}
286
+ }, 200);
287
+ </script>
288
+ <?php
289
+ }
290
+
291
+ /**
292
+ * Send the email code if missing or requested. Stop the authentication
293
+ * validation if a new token has been generated and sent.
294
+ *
295
+ * @param WP_USer $user WP_User object of the logged-in user.
296
+ * @return boolean
297
+ */
298
+ public function pre_process_authentication( $user ) {
299
+ if ( isset( $user->ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) {
300
+ $this->generate_and_email_token( $user );
301
+ return true;
302
+ }
303
+
304
+ return false;
305
+ }
306
+
307
+ /**
308
+ * Validates the users input token.
309
+ *
310
+ * @since 0.1-dev
311
+ *
312
+ * @param WP_User $user WP_User object of the logged-in user.
313
+ * @return boolean
314
+ */
315
+ public function validate_authentication( $user ) {
316
+ if ( ! isset( $user->ID ) || ! isset( $_REQUEST['two-factor-email-code'] ) ) {
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
+ /**
327
+ * Whether this Two Factor provider is configured and available for the user specified.
328
+ *
329
+ * @since 0.1-dev
330
+ *
331
+ * @param WP_User $user WP_User object of the logged-in user.
332
+ * @return boolean
333
+ */
334
+ public function is_available_for_user( $user ) {
335
+ return true;
336
+ }
337
+
338
+ /**
339
+ * Inserts markup at the end of the user profile field for this provider.
340
+ *
341
+ * @since 0.1-dev
342
+ *
343
+ * @param WP_User $user WP_User object of the logged-in user.
344
+ */
345
+ public function user_options( $user ) {
346
+ $email = $user->user_email;
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
360
+ }
361
+ }
providers/class-two-factor-fido-u2f-admin-list-table.php ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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';
11
+ }
12
+
13
+ /**
14
+ * Class for displaying the list of security key items.
15
+ *
16
+ * @since 0.1-dev
17
+ * @access private
18
+ *
19
+ * @package Two_Factor
20
+ */
21
+ class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
22
+
23
+ /**
24
+ * Get a list of columns.
25
+ *
26
+ * @since 0.1-dev
27
+ *
28
+ * @return array
29
+ */
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
+ }
37
+
38
+ /**
39
+ * Prepares the list of items for displaying.
40
+ *
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
+
51
+ /**
52
+ * Generates content for a single row of the table
53
+ *
54
+ * @since 0.1-dev
55
+ * @access protected
56
+ *
57
+ * @param object $item The current item.
58
+ * @param string $column_name The current column name.
59
+ * @return string
60
+ */
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
+ }
81
+ }
82
+
83
+ /**
84
+ * Generates custom table navigation to prevent conflicting nonces.
85
+ *
86
+ * @since 0.1-dev
87
+ * @access protected
88
+ *
89
+ * @param string $which The location of the bulk actions: 'top' or 'bottom'.
90
+ */
91
+ protected function display_tablenav( $which ) {
92
+ // Not used for the Security key list.
93
+ }
94
+
95
+ /**
96
+ * Generates content for a single row of the table
97
+ *
98
+ * @since 0.1-dev
99
+ * @access public
100
+ *
101
+ * @param object $item The current item.
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
109
+ }
110
+
111
+ /**
112
+ * Outputs the hidden row displayed when inline editing
113
+ *
114
+ * @since 0.1-dev
115
+ */
116
+ public function inline_edit() {
117
+ ?>
118
+ <table style="display: none">
119
+ <tbody id="inlineedit">
120
+ <tr id="inline-edit" class="inline-edit-row" style="display: none">
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>
127
+ </label>
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 ] ) ) {
139
+ continue;
140
+ }
141
+
142
+ /** This action is documented in wp-admin/includes/class-wp-posts-list-table.php */
143
+ do_action( 'quick_edit_custom_box', $column_name, 'edit-security-keys' );
144
+ }
145
+ ?>
146
+ <p class="inline-edit-save submit">
147
+ <a href="#inline-edit" class="cancel button-secondary alignleft"><?php esc_html_e( 'Cancel', 'two-factor' ); ?></a>
148
+ <a href="#inline-edit" class="save button-primary alignright"><?php esc_html_e( 'Update', 'two-factor' ); ?></a>
149
+ <span class="spinner"></span>
150
+ <span class="error" style="display:none;"></span>
151
+ <?php wp_nonce_field( 'keyinlineeditnonce', '_inline_edit', false ); ?>
152
+ <br class="clear" />
153
+ </p>
154
+ </td>
155
+ </tr>
156
+ </tbody>
157
+ </table>
158
+ <?php
159
+ }
160
+ }
providers/class-two-factor-fido-u2f-admin.php ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ *
11
+ * @since 0.1-dev
12
+ *
13
+ * @package Two_Factor
14
+ */
15
+ class Two_Factor_FIDO_U2F_Admin {
16
+
17
+ /**
18
+ * The user meta register data.
19
+ *
20
+ * @type string
21
+ */
22
+ const REGISTER_DATA_USER_META_KEY = '_two_factor_fido_u2f_register_request';
23
+
24
+ /**
25
+ * Add various hooks.
26
+ *
27
+ * @since 0.1-dev
28
+ *
29
+ * @access public
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
+ /**
43
+ * Enqueue assets.
44
+ *
45
+ * @since 0.1-dev
46
+ *
47
+ * @access public
48
+ * @static
49
+ *
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 );
70
+ } catch ( Exception $e ) {
71
+ return false;
72
+ }
73
+
74
+ wp_enqueue_style(
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
+
89
+ /**
90
+ * Pass a U2F challenge and user data to our scripts
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' ),
106
+ 2 => esc_html__( 'Bad U2F request.', 'two-factor' ),
107
+ 3 => esc_html__( 'Unsupported U2F configuration.', '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
+
115
+ wp_localize_script(
116
+ 'fido-u2f-admin',
117
+ 'u2fL10n',
118
+ $translation_array
119
+ );
120
+
121
+ /**
122
+ * Script for admin UI
123
+ */
124
+
125
+ wp_enqueue_script(
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
+
133
+ wp_localize_script(
134
+ 'inline-edit-key',
135
+ 'inlineEditL10n',
136
+ array(
137
+ 'error' => esc_html__( 'Error while saving the changes.', 'two-factor' ),
138
+ )
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
+ *
157
+ * This executes during the `show_user_security_settings` action.
158
+ *
159
+ * @since 0.1-dev
160
+ *
161
+ * @access public
162
+ * @static
163
+ *
164
+ * @param WP_User $user WP_User object of the logged-in user.
165
+ */
166
+ public static function show_user_profile( $user ) {
167
+ wp_nonce_field( "user_security_keys-{$user->ID}", '_nonce_user_security_keys' );
168
+ $new_key = false;
169
+
170
+ $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user->ID );
171
+ if ( $security_keys ) {
172
+ foreach ( $security_keys as &$security_key ) {
173
+ if ( property_exists( $security_key, 'new' ) ) {
174
+ $new_key = true;
175
+ unset( $security_key->new );
176
+
177
+ // If we've got a new one, update the db record to not save it there any longer.
178
+ Two_Factor_FIDO_U2F::update_security_key( $user->ID, $security_key );
179
+ }
180
+ }
181
+ unset( $security_key );
182
+ }
183
+
184
+ ?>
185
+ <div class="security-keys" id="security-keys-section">
186
+ <h3><?php esc_html_e( 'Security Keys', 'two-factor' ); ?></h3>
187
+
188
+ <?php if ( ! is_ssl() ) : ?>
189
+ <p class="u2f-error-https">
190
+ <em><?php esc_html_e( 'U2F requires an HTTPS connection. You won\'t be able to add new security keys over HTTP.', 'two-factor' ); ?></em>
191
+ </p>
192
+ <?php endif; ?>
193
+
194
+ <div class="register-security-key">
195
+ <input type="hidden" name="do_new_security_key" id="do_new_security_key" />
196
+ <input type="hidden" name="u2f_response" id="u2f_response" />
197
+ <button type="button" class="button button-secondary" id="register_security_key"><?php echo esc_html( _x( 'Register New Key', 'security key', 'two-factor' ) ); ?></button>
198
+ <span class="spinner"></span>
199
+ <span class="security-key-status"></span>
200
+ </div>
201
+
202
+ <?php if ( $new_key ) : ?>
203
+ <div class="notice notice-success is-dismissible">
204
+ <p class="new-security-key"><?php esc_html_e( 'Your new security key registered.', 'two-factor' ); ?></p>
205
+ </div>
206
+ <?php endif; ?>
207
+
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();
216
+ $u2f_list_table->inline_edit();
217
+ ?>
218
+ </div>
219
+ <?php
220
+ }
221
+
222
+ /**
223
+ * Catch the non-ajax submission from the new form.
224
+ *
225
+ * This executes during the `personal_options_update` & `edit_user_profile_update` actions.
226
+ *
227
+ * @since 0.1-dev
228
+ *
229
+ * @access public
230
+ * @static
231
+ *
232
+ * @param int $user_id User ID.
233
+ * @return false
234
+ */
235
+ public static function catch_submission( $user_id ) {
236
+ if ( ! empty( $_REQUEST['do_new_security_key'] ) ) {
237
+ check_admin_referer( "user_security_keys-{$user_id}", '_nonce_user_security_keys' );
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 );
245
+ } catch ( Exception $e ) {
246
+ return false;
247
+ }
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
+ }
262
+
263
+ /**
264
+ * Catch the delete security key request.
265
+ *
266
+ * This executes during the `load-profile.php` & `load-user-edit.php` actions.
267
+ *
268
+ * @since 0.1-dev
269
+ *
270
+ * @access public
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
+
288
+ /**
289
+ * Generate a link to rename a specified security key.
290
+ *
291
+ * @since 0.1-dev
292
+ *
293
+ * @access public
294
+ * @static
295
+ *
296
+ * @param array $item The current item.
297
+ * @return string
298
+ */
299
+ public static function rename_link( $item ) {
300
+ return sprintf( '<a href="#" class="editinline">%s</a>', esc_html__( 'Rename', 'two-factor' ) );
301
+ }
302
+
303
+ /**
304
+ * Generate a link to delete a specified security key.
305
+ *
306
+ * @since 0.1-dev
307
+ *
308
+ * @access public
309
+ * @static
310
+ *
311
+ * @param array $item The current item.
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
+ }
319
+
320
+ /**
321
+ * Ajax handler for quick edit saving for a security key.
322
+ *
323
+ * @since 0.1-dev
324
+ *
325
+ * @access public
326
+ * @static
327
+ */
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
+ }
349
+
350
+ $key->name = $_POST['name'];
351
+
352
+ $updated = Two_Factor_FIDO_U2F::update_security_key( $user_id, $key );
353
+ if ( ! $updated ) {
354
+ wp_die( esc_html__( 'Item not updated.', 'two-factor' ) );
355
+ }
356
+ $wp_list_table->single_row( $key );
357
+ wp_die();
358
+ }
359
+ }
providers/class-two-factor-fido-u2f.php ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ *
11
+ * @since 0.1-dev
12
+ *
13
+ * @package Two_Factor
14
+ */
15
+ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
16
+
17
+ /**
18
+ * U2F Library
19
+ *
20
+ * @var u2flib_server\U2F
21
+ */
22
+ public static $u2f;
23
+
24
+ /**
25
+ * The user meta registered key.
26
+ *
27
+ * @type string
28
+ */
29
+ const REGISTERED_KEY_USER_META_KEY = '_two_factor_fido_u2f_registered_key';
30
+
31
+ /**
32
+ * The user meta authenticate data.
33
+ *
34
+ * @type string
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 ) ) {
54
+ $instance = new self();
55
+ }
56
+
57
+ return $instance;
58
+ }
59
+
60
+ /**
61
+ * Class constructor.
62
+ *
63
+ * @since 0.1-dev
64
+ */
65
+ protected function __construct() {
66
+ if ( version_compare( PHP_VERSION, '5.3.0', '<' ) ) {
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.
100
+ *
101
+ * @return string AppID URI
102
+ */
103
+ public static function get_u2f_app_id() {
104
+ $url_parts = wp_parse_url( home_url() );
105
+
106
+ if ( ! empty( $url_parts['port'] ) ) {
107
+ return sprintf( 'https://%s:%d', $url_parts['host'], $url_parts['port'] );
108
+ } else {
109
+ return sprintf( 'https://%s', $url_parts['host'] );
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Returns the name of the provider.
115
+ *
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
+ /**
147
+ * Prints the form that prompts the user to authenticate.
148
+ *
149
+ * @since 0.1-dev
150
+ *
151
+ * @param WP_User $user WP_User object of the logged-in user.
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>
161
+ <?php
162
+
163
+ return;
164
+ }
165
+
166
+ try {
167
+ $keys = self::get_security_keys( $user->ID );
168
+ $data = self::$u2f->getAuthenticateData( $keys );
169
+ update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data );
170
+ } catch ( Exception $e ) {
171
+ ?>
172
+ <p><?php esc_html_e( 'An error occurred while creating authentication data.', 'two-factor' ); ?></p>
173
+ <?php
174
+ return null;
175
+ }
176
+
177
+ wp_localize_script(
178
+ 'fido-u2f-login',
179
+ 'u2fL10n',
180
+ array(
181
+ 'request' => $data,
182
+ )
183
+ );
184
+
185
+ wp_enqueue_script( 'fido-u2f-login' );
186
+
187
+ ?>
188
+ <p><?php esc_html_e( 'Now insert (and tap) your Security Key.', 'two-factor' ); ?></p>
189
+ <input type="hidden" name="u2f_response" id="u2f_response" />
190
+ <?php
191
+ }
192
+
193
+ /**
194
+ * Validates the users input token.
195
+ *
196
+ * @since 0.1-dev
197
+ *
198
+ * @param WP_User $user WP_User object of the logged-in user.
199
+ * @return boolean
200
+ */
201
+ public function validate_authentication( $user ) {
202
+ $requests = get_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, true );
203
+
204
+ $response = json_decode( stripslashes( $_REQUEST['u2f_response'] ) );
205
+
206
+ $keys = self::get_security_keys( $user->ID );
207
+
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
+
215
+ return true;
216
+ } catch ( Exception $e ) {
217
+ return false;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Whether this Two Factor provider is configured and available for the user specified.
223
+ *
224
+ * @since 0.1-dev
225
+ *
226
+ * @param WP_User $user WP_User object of the logged-in user.
227
+ * @return boolean
228
+ */
229
+ public function is_available_for_user( $user ) {
230
+ return (bool) self::get_security_keys( $user->ID );
231
+ }
232
+
233
+ /**
234
+ * Inserts markup at the end of the user profile field for this provider.
235
+ *
236
+ * @since 0.1-dev
237
+ *
238
+ * @param WP_User $user WP_User object of the logged-in user.
239
+ */
240
+ public function user_options( $user ) {
241
+ ?>
242
+ <p>
243
+ <?php esc_html_e( 'Requires an HTTPS connection. Configure your security keys in the "Security Keys" section below.', 'two-factor' ); ?>
244
+ </p>
245
+ <?php
246
+ }
247
+
248
+ /**
249
+ * Add registered security key to a user.
250
+ *
251
+ * @since 0.1-dev
252
+ *
253
+ * @param int $user_id User ID.
254
+ * @param object $register The data of registered security key.
255
+ * @return int|bool Meta ID on success, false on failure.
256
+ */
257
+ public static function add_security_key( $user_id, $register ) {
258
+ if ( ! is_numeric( $user_id ) ) {
259
+ return false;
260
+ }
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
+ ) {
269
+ return false;
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 );
284
+ }
285
+
286
+ /**
287
+ * Retrieve registered security keys for a user.
288
+ *
289
+ * @since 0.1-dev
290
+ *
291
+ * @param int $user_id User ID.
292
+ * @return array|bool Array of keys on success, false on failure.
293
+ */
294
+ public static function get_security_keys( $user_id ) {
295
+ if ( ! is_numeric( $user_id ) ) {
296
+ return false;
297
+ }
298
+
299
+ $keys = get_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY );
300
+ if ( $keys ) {
301
+ foreach ( $keys as &$key ) {
302
+ $key = (object) $key;
303
+ }
304
+ unset( $key );
305
+ }
306
+
307
+ return $keys;
308
+ }
309
+
310
+ /**
311
+ * Update registered security key.
312
+ *
313
+ * Use the $prev_value parameter to differentiate between meta fields with the
314
+ * same key and user ID.
315
+ *
316
+ * If the meta field for the user does not exist, it will be added.
317
+ *
318
+ * @since 0.1-dev
319
+ *
320
+ * @param int $user_id User ID.
321
+ * @param object $data The data of registered security key.
322
+ * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure.
323
+ */
324
+ public static function update_security_key( $user_id, $data ) {
325
+ if ( ! is_numeric( $user_id ) ) {
326
+ return false;
327
+ }
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
+ ) {
336
+ return false;
337
+ }
338
+
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
+ }
346
+ }
347
+
348
+ return self::add_security_key( $user_id, $data );
349
+ }
350
+
351
+ /**
352
+ * Remove registered security key matching criteria from a user.
353
+ *
354
+ * @since 0.1-dev
355
+ *
356
+ * @param int $user_id User ID.
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 ) ) {
364
+ return false;
365
+ }
366
+
367
+ $user_id = absint( $user_id );
368
+ if ( ! $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
+ }
390
+
391
+ foreach ( $meta_ids as $meta_id ) {
392
+ delete_metadata_by_mid( 'user', $meta_id );
393
+ }
394
+
395
+ return true;
396
+ }
397
+ }
providers/class-two-factor-provider.php ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ *
11
+ * @since 0.1-dev
12
+ *
13
+ * @package Two_Factor
14
+ */
15
+ abstract class Two_Factor_Provider {
16
+
17
+ /**
18
+ * Class constructor.
19
+ *
20
+ * @since 0.1-dev
21
+ */
22
+ protected function __construct() {
23
+ return $this;
24
+ }
25
+
26
+ /**
27
+ * Returns the name of the provider.
28
+ *
29
+ * @since 0.1-dev
30
+ *
31
+ * @return string
32
+ */
33
+ abstract public function get_label();
34
+
35
+ /**
36
+ * Prints the name of the provider.
37
+ *
38
+ * @since 0.1-dev
39
+ */
40
+ public function print_label() {
41
+ echo esc_html( $this->get_label() );
42
+ }
43
+
44
+ /**
45
+ * Prints the form that prompts the user to authenticate.
46
+ *
47
+ * @since 0.1-dev
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.
55
+ * Return `true` to prevent the authentication and render the
56
+ * authentication page.
57
+ *
58
+ * @param WP_User $user WP_User object of the logged-in user.
59
+ * @return boolean
60
+ */
61
+ public function pre_process_authentication( $user ) {
62
+ return false;
63
+ }
64
+
65
+ /**
66
+ * Validates the users input token.
67
+ *
68
+ * @since 0.1-dev
69
+ *
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.
77
+ *
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.
85
+ *
86
+ * @since 0.1-dev
87
+ *
88
+ * @param int $length The code length.
89
+ * @param string|array $chars Valid auth code characters.
90
+ * @return string
91
+ */
92
+ public function get_code( $length = 8, $chars = '1234567890' ) {
93
+ $code = '';
94
+ if ( is_array( $chars ) ) {
95
+ $chars = implode( '', $chars );
96
+ }
97
+ for ( $i = 0; $i < $length; $i++ ) {
98
+ $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
99
+ }
100
+ return $code;
101
+ }
102
+ }
providers/class-two-factor-totp.php ADDED
@@ -0,0 +1,558 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Class for creating a Time Based One-Time Password provider.
4
+ *
5
+ * @package Two_Factor
6
+ */
7
+
8
+ /**
9
+ * Class Two_Factor_Totp
10
+ */
11
+ class Two_Factor_Totp extends Two_Factor_Provider {
12
+
13
+ /**
14
+ * The user meta token key.
15
+ *
16
+ * @var string
17
+ */
18
+ const SECRET_META_KEY = '_two_factor_totp_key';
19
+
20
+ /**
21
+ * The user meta token key.
22
+ *
23
+ * @var string
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
+ * Chracters 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
+ protected function __construct() {
51
+ add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_two_factor_options' ) );
52
+ add_action( 'personal_options_update', array( $this, 'user_two_factor_options_update' ) );
53
+ add_action( 'edit_user_profile_update', array( $this, 'user_two_factor_options_update' ) );
54
+ add_action( 'two_factor_user_settings_action', array( $this, 'user_settings_action' ), 10, 2 );
55
+
56
+ return parent::__construct();
57
+ }
58
+
59
+ /**
60
+ * Ensures only one instance of this class exists in memory at any one time.
61
+ */
62
+ public static function get_instance() {
63
+ static $instance;
64
+ if ( ! isset( $instance ) ) {
65
+ $instance = new self();
66
+ }
67
+ return $instance;
68
+ }
69
+
70
+ /**
71
+ * Returns the name of the provider.
72
+ */
73
+ public function get_label() {
74
+ return _x( 'Time Based One-Time Password (TOTP)', 'Provider Label', 'two-factor' );
75
+ }
76
+
77
+ /**
78
+ * Trigger our custom user settings actions.
79
+ *
80
+ * @param integer $user_id User ID.
81
+ * @param string $action Action ID.
82
+ *
83
+ * @return void
84
+ */
85
+ public function user_settings_action( $user_id, $action ) {
86
+ if ( self::ACTION_SECRET_DELETE === $action ) {
87
+ $this->delete_user_totp_key( $user_id );
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get the URL for deleting the secret token.
93
+ *
94
+ * @param integer $user_id User ID.
95
+ *
96
+ * @return string
97
+ */
98
+ protected function get_token_delete_url_for_user( $user_id ) {
99
+ return Two_Factor_Core::get_user_update_action_url( $user_id, self::ACTION_SECRET_DELETE );
100
+ }
101
+
102
+ /**
103
+ * Display TOTP options on the user settings page.
104
+ *
105
+ * @param WP_User $user The current user being edited.
106
+ * @return false
107
+ */
108
+ public function user_two_factor_options( $user ) {
109
+ if ( ! isset( $user->ID ) ) {
110
+ return false;
111
+ }
112
+
113
+ wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false );
114
+
115
+ $key = $this->get_user_totp_key( $user->ID );
116
+ $this->admin_notices( $user->ID );
117
+
118
+ ?>
119
+ <div id="two-factor-totp-options">
120
+ <?php
121
+ if ( empty( $key ) ) :
122
+ $key = $this->generate_key();
123
+ $site_name = get_bloginfo( 'name', 'display' );
124
+ $totp_title = apply_filters( 'two_factor_totp_title', $site_name . ':' . $user->user_login, $user );
125
+ ?>
126
+ <p>
127
+ <?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' ); ?>
128
+ </p>
129
+ <p>
130
+ <img src="<?php echo esc_url( $this->get_google_qr_code( $totp_title, $key, $site_name ) ); ?>" id="two-factor-totp-qrcode" />
131
+ </p>
132
+ <p>
133
+ <code><?php echo esc_html( $key ); ?></code>
134
+ </p>
135
+ <p>
136
+ <input type="hidden" name="two-factor-totp-key" value="<?php echo esc_attr( $key ); ?>" />
137
+ <label for="two-factor-totp-authcode">
138
+ <?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?>
139
+ <input type="tel" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9]*" />
140
+ </label>
141
+ <input type="submit" class="button" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Submit', 'two-factor' ); ?>" />
142
+ </p>
143
+ <?php else : ?>
144
+ <p class="success">
145
+ <?php esc_html_e( 'Secret key is configured and registered. It is not possible to view it again for security reasons.', 'two-factor' ); ?>
146
+ </p>
147
+ <p>
148
+ <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>
149
+ <em class="description">
150
+ <?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' ); ?>
151
+ </em>
152
+ </p>
153
+ <?php endif; ?>
154
+ </div>
155
+ <?php
156
+ }
157
+
158
+ /**
159
+ * Save the options specified in `::user_two_factor_options()`
160
+ *
161
+ * @param integer $user_id The user ID whose options are being updated.
162
+ *
163
+ * @return void
164
+ */
165
+ public function user_two_factor_options_update( $user_id ) {
166
+ $notices = array();
167
+ $errors = array();
168
+
169
+ if ( isset( $_POST['_nonce_user_two_factor_totp_options'] ) ) {
170
+ check_admin_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' );
171
+
172
+ // Validate and store a new secret key.
173
+ if ( ! empty( $_POST['two-factor-totp-authcode'] ) && ! empty( $_POST['two-factor-totp-key'] ) ) {
174
+ // Don't use filter_input() because we can't mock it during tests for now.
175
+ $authcode = filter_var( sanitize_text_field( $_POST['two-factor-totp-authcode'] ), FILTER_SANITIZE_NUMBER_INT );
176
+ $key = sanitize_text_field( $_POST['two-factor-totp-key'] );
177
+
178
+ if ( $this->is_valid_key( $key ) ) {
179
+ if ( $this->is_valid_authcode( $key, $authcode ) ) {
180
+ if ( ! $this->set_user_totp_key( $user_id, $key ) ) {
181
+ $errors[] = __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' );
182
+ }
183
+ } else {
184
+ $errors[] = __( 'Invalid Two Factor Authentication code.', 'two-factor' );
185
+ }
186
+ } else {
187
+ $errors[] = __( 'Invalid Two Factor Authentication secret key.', 'two-factor' );
188
+ }
189
+ }
190
+
191
+ if ( ! empty( $errors ) ) {
192
+ $notices['error'] = $errors;
193
+ }
194
+
195
+ if ( ! empty( $notices ) ) {
196
+ update_user_meta( $user_id, self::NOTICES_META_KEY, $notices );
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Get the TOTP secret key for a user.
203
+ *
204
+ * @param int $user_id User ID.
205
+ *
206
+ * @return string
207
+ */
208
+ public function get_user_totp_key( $user_id ) {
209
+ return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
210
+ }
211
+
212
+ /**
213
+ * Set the TOTP secret key for a user.
214
+ *
215
+ * @param int $user_id User ID.
216
+ * @param string $key TOTP secret key.
217
+ *
218
+ * @return boolean If the key was stored successfully.
219
+ */
220
+ public function set_user_totp_key( $user_id, $key ) {
221
+ return update_user_meta( $user_id, self::SECRET_META_KEY, $key );
222
+ }
223
+
224
+ /**
225
+ * Delete the TOTP secret key for a user.
226
+ *
227
+ * @param int $user_id User ID.
228
+ *
229
+ * @return boolean If the key was deleted successfully.
230
+ */
231
+ public function delete_user_totp_key( $user_id ) {
232
+ return delete_user_meta( $user_id, self::SECRET_META_KEY );
233
+ }
234
+
235
+ /**
236
+ * Check if the TOTP secret key has a proper format.
237
+ *
238
+ * @param string $key TOTP secret key.
239
+ *
240
+ * @return boolean
241
+ */
242
+ public function is_valid_key( $key ) {
243
+ $check = sprintf( '/^[%s]+$/', self::$base_32_chars );
244
+
245
+ if ( 1 === preg_match( $check, $key ) ) {
246
+ return true;
247
+ }
248
+
249
+ return false;
250
+ }
251
+
252
+ /**
253
+ * Display any available admin notices.
254
+ *
255
+ * @param integer $user_id User ID.
256
+ *
257
+ * @return void
258
+ */
259
+ public function admin_notices( $user_id ) {
260
+ $notices = get_user_meta( $user_id, self::NOTICES_META_KEY, true );
261
+
262
+ if ( ! empty( $notices ) ) {
263
+ delete_user_meta( $user_id, self::NOTICES_META_KEY );
264
+
265
+ foreach ( $notices as $class => $messages ) {
266
+ ?>
267
+ <div class="<?php echo esc_attr( $class ); ?>">
268
+ <?php
269
+ foreach ( $messages as $msg ) {
270
+ ?>
271
+ <p>
272
+ <span><?php echo esc_html( $msg ); ?><span>
273
+ </p>
274
+ <?php
275
+ }
276
+ ?>
277
+ </div>
278
+ <?php
279
+ }
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Validates authentication.
285
+ *
286
+ * @param WP_User $user WP_User object of the logged-in user.
287
+ *
288
+ * @return bool Whether the user gave a valid code
289
+ */
290
+ public function validate_authentication( $user ) {
291
+ if ( ! empty( $_REQUEST['authcode'] ) ) {
292
+ return $this->is_valid_authcode(
293
+ $this->get_user_totp_key( $user->ID ),
294
+ sanitize_text_field( $_REQUEST['authcode'] )
295
+ );
296
+ }
297
+
298
+ return false;
299
+ }
300
+
301
+ /**
302
+ * Checks if a given code is valid for a given key, allowing for a certain amount of time drift
303
+ *
304
+ * @param string $key The share secret key to use.
305
+ * @param string $authcode The code to test.
306
+ *
307
+ * @return bool Whether the code is valid within the time frame
308
+ */
309
+ public static function is_valid_authcode( $key, $authcode ) {
310
+ /**
311
+ * Filter the maximum ticks to allow when checking valid codes.
312
+ *
313
+ * Ticks are the allowed offset from the correct time in 30 second increments,
314
+ * so the default of 4 allows codes that are two minutes to either side of server time
315
+ *
316
+ * @deprecated 0.7.0 Use {@see 'two_factor_totp_time_step_allowance'} instead.
317
+ * @param int $max_ticks Max ticks of time correction to allow. Default 4.
318
+ */
319
+ $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' );
320
+
321
+ $max_ticks = apply_filters( 'two_factor_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
322
+
323
+ // Array of all ticks to allow, sorted using absolute value to test closest match first.
324
+ $ticks = range( - $max_ticks, $max_ticks );
325
+ usort( $ticks, array( __CLASS__, 'abssort' ) );
326
+
327
+ $time = time() / self::DEFAULT_TIME_STEP_SEC;
328
+
329
+ foreach ( $ticks as $offset ) {
330
+ $log_time = $time + $offset;
331
+ if ( self::calc_totp( $key, $log_time ) === $authcode ) {
332
+ return true;
333
+ }
334
+ }
335
+ return false;
336
+ }
337
+
338
+ /**
339
+ * Generates key
340
+ *
341
+ * @param int $bitsize Nume of bits to use for key.
342
+ *
343
+ * @return string $bitsize long string composed of available base32 chars.
344
+ */
345
+ public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
346
+ $bytes = ceil( $bitsize / 8 );
347
+ $secret = wp_generate_password( $bytes, true, true );
348
+
349
+ return self::base32_encode( $secret );
350
+ }
351
+
352
+ /**
353
+ * Pack stuff
354
+ *
355
+ * @param string $value The value to be packed.
356
+ *
357
+ * @return string Binary packed string.
358
+ */
359
+ public static function pack64( $value ) {
360
+ // 64bit mode (PHP_INT_SIZE == 8).
361
+ if ( PHP_INT_SIZE >= 8 ) {
362
+ // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
363
+ if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
364
+ return pack( 'J', $value ); // phpcs:ignore PHPCompatibility.ParameterValues.NewPackFormat.NewFormatFound
365
+ }
366
+ $highmap = 0xffffffff << 32;
367
+ $higher = ( $value & $highmap ) >> 32;
368
+ } else {
369
+ /*
370
+ * 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher
371
+ * and not pack anything beyond it's limits.
372
+ */
373
+ $higher = 0;
374
+ }
375
+
376
+ $lowmap = 0xffffffff;
377
+ $lower = $value & $lowmap;
378
+
379
+ return pack( 'NN', $higher, $lower );
380
+ }
381
+
382
+ /**
383
+ * Calculate a valid code given the shared secret key
384
+ *
385
+ * @param string $key The shared secret key to use for calculating code.
386
+ * @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size.
387
+ * @param int $digits The number of digits in the returned code.
388
+ * @param string $hash The hash used to calculate the code.
389
+ * @param int $time_step The size of the time step.
390
+ *
391
+ * @return string The totp code
392
+ */
393
+ public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
394
+ $secret = self::base32_decode( $key );
395
+
396
+ if ( false === $step_count ) {
397
+ $step_count = floor( time() / $time_step );
398
+ }
399
+
400
+ $timestamp = self::pack64( $step_count );
401
+
402
+ $hash = hash_hmac( $hash, $timestamp, $secret, true );
403
+
404
+ $offset = ord( $hash[19] ) & 0xf;
405
+
406
+ $code = (
407
+ ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
408
+ ( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
409
+ ( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
410
+ ( ord( $hash[ $offset + 3 ] ) & 0xff )
411
+ ) % pow( 10, $digits );
412
+
413
+ return str_pad( $code, $digits, '0', STR_PAD_LEFT );
414
+ }
415
+
416
+ /**
417
+ * Uses the Google Charts API to build a QR Code for use with an otpauth url
418
+ *
419
+ * @param string $name The name to display in the Authentication app.
420
+ * @param string $key The secret key to share with the Authentication app.
421
+ * @param string $title The title to display in the Authentication app.
422
+ *
423
+ * @return string A URL to use as an img src to display the QR code
424
+ */
425
+ public static function get_google_qr_code( $name, $key, $title = null ) {
426
+ // Encode to support spaces, question marks and other characters.
427
+ $name = rawurlencode( $name );
428
+ $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key );
429
+ if ( isset( $title ) ) {
430
+ $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) );
431
+ }
432
+ return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_url;
433
+ }
434
+
435
+ /**
436
+ * Whether this Two Factor provider is configured and available for the user specified.
437
+ *
438
+ * @param WP_User $user WP_User object of the logged-in user.
439
+ *
440
+ * @return boolean
441
+ */
442
+ public function is_available_for_user( $user ) {
443
+ // Only available if the secret key has been saved for the user.
444
+ $key = $this->get_user_totp_key( $user->ID );
445
+
446
+ return ! empty( $key );
447
+ }
448
+
449
+ /**
450
+ * Prints the form that prompts the user to authenticate.
451
+ *
452
+ * @param WP_User $user WP_User object of the logged-in user.
453
+ */
454
+ public function authentication_page( $user ) {
455
+ require_once ABSPATH . '/wp-admin/includes/template.php';
456
+ ?>
457
+ <p>
458
+ <?php esc_html_e( 'Please enter the code generated by your authenticator app.', 'two-factor' ); ?>
459
+ </p>
460
+ <p>
461
+ <label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
462
+ <input type="tel" autocomplete="off" name="authcode" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
463
+ </p>
464
+ <script type="text/javascript">
465
+ setTimeout( function(){
466
+ var d;
467
+ try{
468
+ d = document.getElementById('authcode');
469
+ d.focus();
470
+ } catch(e){}
471
+ }, 200);
472
+ </script>
473
+ <?php
474
+ submit_button( __( 'Authenticate', 'two-factor' ) );
475
+ }
476
+
477
+ /**
478
+ * Returns a base32 encoded string.
479
+ *
480
+ * @param string $string String to be encoded using base32.
481
+ *
482
+ * @return string base32 encoded string without padding.
483
+ */
484
+ public static function base32_encode( $string ) {
485
+ if ( empty( $string ) ) {
486
+ return '';
487
+ }
488
+
489
+ $binary_string = '';
490
+
491
+ foreach ( str_split( $string ) as $character ) {
492
+ $binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
493
+ }
494
+
495
+ $five_bit_sections = str_split( $binary_string, 5 );
496
+ $base32_string = '';
497
+
498
+ foreach ( $five_bit_sections as $five_bit_section ) {
499
+ $base32_string .= self::$base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
500
+ }
501
+
502
+ return $base32_string;
503
+ }
504
+
505
+ /**
506
+ * Decode a base32 string and return a binary representation
507
+ *
508
+ * @param string $base32_string The base 32 string to decode.
509
+ *
510
+ * @throws Exception If string contains non-base32 characters.
511
+ *
512
+ * @return string Binary representation of decoded string
513
+ */
514
+ public static function base32_decode( $base32_string ) {
515
+
516
+ $base32_string = strtoupper( $base32_string );
517
+
518
+ if ( ! preg_match( '/^[' . self::$base_32_chars . ']+$/', $base32_string, $match ) ) {
519
+ throw new Exception( 'Invalid characters in the base32 string.' );
520
+ }
521
+
522
+ $l = strlen( $base32_string );
523
+ $n = 0;
524
+ $j = 0;
525
+ $binary = '';
526
+
527
+ for ( $i = 0; $i < $l; $i++ ) {
528
+
529
+ $n = $n << 5; // Move buffer left by 5 to make room.
530
+ $n = $n + strpos( self::$base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
531
+ $j += 5; // Keep track of number of bits in buffer.
532
+
533
+ if ( $j >= 8 ) {
534
+ $j -= 8;
535
+ $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
536
+ }
537
+ }
538
+
539
+ return $binary;
540
+ }
541
+
542
+ /**
543
+ * Used with usort to sort an array by distance from 0
544
+ *
545
+ * @param int $a First array element.
546
+ * @param int $b Second array element.
547
+ *
548
+ * @return int -1, 0, or 1 as needed by usort
549
+ */
550
+ private static function abssort( $a, $b ) {
551
+ $a = abs( $a );
552
+ $b = abs( $b );
553
+ if ( $a === $b ) {
554
+ return 0;
555
+ }
556
+ return ( $a < $b ) ? -1 : 1;
557
+ }
558
+ }
providers/css/fido-u2f-admin.css ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
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;
12
+ }
providers/js/fido-u2f-admin-inline-edit.js ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* global window, document, jQuery, inlineEditL10n, ajaxurl */
2
+ var inlineEditKey;
3
+
4
+ ( function( $ ) {
5
+ inlineEditKey = {
6
+
7
+ init: function() {
8
+ var t = this,
9
+ row = $( '#security-keys-section #inline-edit' );
10
+
11
+ t.what = '#key-';
12
+
13
+ $( '#security-keys-section #the-list' ).on( 'click', 'a.editinline', function() {
14
+ inlineEditKey.edit( this );
15
+ return false;
16
+ } );
17
+
18
+ // Prepare the edit row.
19
+ row.keyup( function( event ) {
20
+ if ( 27 === event.which ) {
21
+ return inlineEditKey.revert();
22
+ }
23
+ } );
24
+
25
+ $( 'a.cancel', row ).click( function() {
26
+ return inlineEditKey.revert();
27
+ } );
28
+
29
+ $( 'a.save', row ).click( function() {
30
+ return inlineEditKey.save( this );
31
+ } );
32
+
33
+ $( 'input, select', row ).keydown( function( event ) {
34
+ if ( 13 === event.which ) {
35
+ return inlineEditKey.save( this );
36
+ }
37
+ } );
38
+ },
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 ) {
51
+ var editRow, rowData, val,
52
+ t = this;
53
+ t.revert();
54
+
55
+ if ( 'object' === typeof 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>' );
65
+
66
+ val = $( '.name', rowData );
67
+ val.find( 'img' ).replaceWith( function() {
68
+ return this.alt;
69
+ } );
70
+ val = val.text();
71
+ $( ':input[name="name"]', editRow ).val( val );
72
+
73
+ $( editRow ).attr( 'id', 'edit-' + id ).addClass( 'inline-editor' ).show();
74
+ $( '.ptitle', editRow ).eq( 0 ).focus();
75
+
76
+ return false;
77
+ },
78
+
79
+ save: function( id ) {
80
+ var params, fields;
81
+
82
+ if ( 'object' === typeof id ) {
83
+ id = this.getId( id );
84
+ }
85
+
86
+ $( '#security-keys-section table.widefat .spinner' ).addClass( 'is-active' );
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();
95
+ params = fields + '&' + $.param( params );
96
+
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 ) {
104
+ if ( -1 !== r.indexOf( '<tr' ) ) {
105
+ $( inlineEditKey.what + id ).siblings( 'tr.hidden' ).addBack().remove();
106
+ newID = $( r ).attr( 'id' );
107
+
108
+ $( '#edit-' + id ).before( r ).remove();
109
+
110
+ if ( newID ) {
111
+ row = $( '#' + newID );
112
+ } else {
113
+ row = $( inlineEditKey.what + id );
114
+ }
115
+
116
+ row.hide().fadeIn();
117
+ } else {
118
+ $( '#edit-' + id + ' .inline-edit-save .error' ).html( r ).show();
119
+ }
120
+ } else {
121
+ $( '#edit-' + id + ' .inline-edit-save .error' ).html( inlineEditL10n.error ).show();
122
+ }
123
+ }
124
+ );
125
+ return false;
126
+ },
127
+
128
+ revert: function() {
129
+ var id = $( '#security-keys-section table.widefat tr.inline-editor' ).attr( 'id' );
130
+
131
+ if ( id ) {
132
+ $( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' );
133
+ $( '#' + id ).siblings( 'tr.hidden' ).addBack().remove();
134
+ id = id.replace( /\w+\-/, '' );
135
+ $( this.what + id ).show();
136
+ }
137
+
138
+ return false;
139
+ },
140
+
141
+ getId: function( o ) {
142
+ var id = 'TR' === o.tagName ? o.id : $( o ).parents( 'tr' ).attr( 'id' );
143
+ return id.replace( /\w+\-/, '' );
144
+ }
145
+ };
146
+
147
+ $( document ).ready( function() {
148
+ inlineEditKey.init();
149
+ } );
150
+ }( jQuery ) );
providers/js/fido-u2f-admin.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 );
9
+ }
10
+
11
+ $button.click( function() {
12
+ var registerRequest;
13
+
14
+ if ( $( this ).prop( 'disabled' ) ) {
15
+ return false;
16
+ }
17
+
18
+ $( this ).prop( 'disabled', true );
19
+ $( '.register-security-key .spinner' ).addClass( 'is-active' );
20
+ $statusNotice.text( '' );
21
+
22
+ registerRequest = {
23
+ version: u2fL10n.register.request.version,
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
+
31
+ if ( data.errorCode ) {
32
+ if ( u2fL10n.text.error_codes[ data.errorCode ] ) {
33
+ $statusNotice.text( u2fL10n.text.error_codes[ data.errorCode ] );
34
+ } else {
35
+ $statusNotice.text( u2fL10n.text.error_codes[ u2fL10n.text.error ] );
36
+ }
37
+
38
+ return false;
39
+ }
40
+
41
+ $( '#do_new_security_key' ).val( 'true' );
42
+ $( '#u2f_response' ).val( JSON.stringify( data ) );
43
+
44
+ // See: http://stackoverflow.com/questions/833032/submit-is-not-a-function-error-in-javascript
45
+ $( '<form>' )[0].submit.call( $( '#your-profile' )[0] );
46
+ } );
47
+ } );
48
+ }( jQuery ) );
providers/js/fido-u2f-login.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* global window, u2f, u2fL10n, jQuery */
2
+ ( function( $ ) {
3
+ if ( ! window.u2fL10n ) {
4
+ window.console.error( 'u2fL10n is not defined' );
5
+ return;
6
+ }
7
+
8
+ u2f.sign( u2fL10n.request[0].appId, u2fL10n.request[0].challenge, u2fL10n.request, function( data ) {
9
+ if ( data.errorCode ) {
10
+ window.console.error( 'Registration Failed', data.errorCode );
11
+ } else {
12
+ $( '#u2f_response' ).val( JSON.stringify( data ) );
13
+ $( '#loginform' ).submit();
14
+ }
15
+ } );
16
+ }( jQuery ) );
readme.txt ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: trunk
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 ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.2
16
+ * Author URI: https://github.com/wordpress/two-factor/graphs/contributors
17
+ * Network: True
18
+ * Text Domain: two-factor
19
+ */
20
+
21
+ /**
22
+ * Shortcut constant to the path of this file.
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.2' );
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 ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ .two-factor-methods-table {
3
+ background-color: #fff;
4
+ border: 1px solid #e5e5e5;
5
+ border-spacing: 0;
6
+ }
7
+
8
+ .two-factor-methods-table thead,
9
+ .two-factor-methods-table tfoot {
10
+ background: #fff;
11
+ }
12
+
13
+ .two-factor-methods-table thead th {
14
+ padding: 0.5em;
15
+ }
16
+
17
+ .two-factor-methods-table .col-primary,
18
+ .two-factor-methods-table .col-enabled {
19
+ width: 5%;
20
+ }
21
+
22
+ .two-factor-methods-table .col-name {
23
+ width: 90%;
24
+ }
25
+
26
+ .two-factor-methods-table tbody th {
27
+ text-align: center;
28
+ }
29
+
30
+ .two-factor-methods-table tbody th,
31
+ .two-factor-methods-table tbody td {
32
+ vertical-align: top;
33
+ }
34
+
35
+ .two-factor-methods-table tbody tr:nth-child(odd) {
36
+ background-color: #f9f9f9;
37
+ }