Version Description
Download this release
Release Info
Developer | kasparsd |
Plugin | Two-Factor |
Version | 0.7.2 |
Comparing to | |
See all releases |
Version 0.7.2
- LICENSE.md +280 -0
- class-two-factor-compat.php +55 -0
- class-two-factor-core.php +1072 -0
- includes/Google/u2f-api.js +748 -0
- includes/Yubico/U2F.php +507 -0
- includes/function.login-footer.php +87 -0
- includes/function.login-header.php +259 -0
- providers/class-two-factor-backup-codes.php +355 -0
- providers/class-two-factor-dummy.php +99 -0
- providers/class-two-factor-email.php +361 -0
- providers/class-two-factor-fido-u2f-admin-list-table.php +160 -0
- providers/class-two-factor-fido-u2f-admin.php +359 -0
- providers/class-two-factor-fido-u2f.php +397 -0
- providers/class-two-factor-provider.php +102 -0
- providers/class-two-factor-totp.php +558 -0
- providers/css/fido-u2f-admin.css +12 -0
- providers/js/fido-u2f-admin-inline-edit.js +150 -0
- providers/js/fido-u2f-admin.js +48 -0
- providers/js/fido-u2f-login.js +16 -0
- readme.txt +44 -0
- two-factor.php +48 -0
- user-edit.css +37 -0
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’ 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 →', '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’ 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( '← 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 ‹ %2$s — WordPress' ), $title, $login_title );
|
54 |
+
|
55 |
+
if ( wp_is_recovery_mode() ) {
|
56 |
+
/* translators: %s: Login screen title. */
|
57 |
+
$login_title = sprintf( __( 'Recovery Mode — %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 |
+
}
|