-
Notifications
You must be signed in to change notification settings - Fork 148
/
class-two-factor-fido-u2f.php
397 lines (339 loc) · 10.7 KB
/
class-two-factor-fido-u2f.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
<?php
/**
* Class for creating a FIDO Universal 2nd Factor provider.
*
* @package Two_Factor
*/
/**
* Class for creating a FIDO Universal 2nd Factor provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
/**
* U2F Library
*
* @var u2flib_server\U2F
*/
public static $u2f;
/**
* The user meta registered key.
*
* @type string
*/
const REGISTERED_KEY_USER_META_KEY = '_two_factor_fido_u2f_registered_key';
/**
* The user meta authenticate data.
*
* @type string
*/
const AUTH_DATA_USER_META_KEY = '_two_factor_fido_u2f_login_request';
/**
* Version number for the bundled assets.
*
* @var string
*/
const U2F_ASSET_VERSION = '0.2.1';
/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @return \Two_Factor_FIDO_U2F
*/
public static function get_instance() {
static $instance;
if ( ! isset( $instance ) ) {
$instance = new self();
}
return $instance;
}
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
if ( version_compare( PHP_VERSION, '5.3.0', '<' ) ) {
return;
}
require_once TWO_FACTOR_DIR . 'includes/Yubico/U2F.php';
self::$u2f = new u2flib_server\U2F( self::get_u2f_app_id() );
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin.php';
Two_Factor_FIDO_U2F_Admin::add_hooks();
// Ensure the script dependencies have been registered before they're enqueued at a later priority.
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'login_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
return parent::__construct();
}
/**
* Get the asset version number.
*
* TODO: There should be a plugin-level helper for getting the current plugin version.
*
* @return string
*/
public static function asset_version() {
return self::U2F_ASSET_VERSION;
}
/**
* Return the U2F AppId. U2F requires the AppID to use HTTPS
* and a top-level domain.
*
* @return string AppID URI
*/
public static function get_u2f_app_id() {
$url_parts = wp_parse_url( home_url() );
if ( ! empty( $url_parts['port'] ) ) {
return sprintf( 'https://%s:%d', $url_parts['host'], $url_parts['port'] );
} else {
return sprintf( 'https://%s', $url_parts['host'] );
}
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'FIDO U2F Security Keys', 'Provider Label', 'two-factor' );
}
/**
* Register script dependencies used during login and when
* registering keys in the WP admin.
*
* @return void
*/
public static function enqueue_scripts() {
wp_register_script(
'fido-u2f-api',
plugins_url( 'includes/Google/u2f-api.js', dirname( __FILE__ ) ),
null,
self::asset_version(),
true
);
wp_register_script(
'fido-u2f-login',
plugins_url( 'js/fido-u2f-login.js', __FILE__ ),
array( 'jquery', 'fido-u2f-api' ),
self::asset_version(),
true
);
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return null
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
// U2F doesn't work without HTTPS.
if ( ! is_ssl() ) {
?>
<p><?php esc_html_e( 'U2F requires an HTTPS connection. Please use an alternative 2nd factor method.', 'two-factor' ); ?></p>
<?php
return;
}
try {
$keys = self::get_security_keys( $user->ID );
$data = self::$u2f->getAuthenticateData( $keys );
update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data );
} catch ( Exception $e ) {
?>
<p><?php esc_html_e( 'An error occurred while creating authentication data.', 'two-factor' ); ?></p>
<?php
return null;
}
wp_localize_script(
'fido-u2f-login',
'u2fL10n',
array(
'request' => $data,
)
);
wp_enqueue_script( 'fido-u2f-login' );
?>
<p><?php esc_html_e( 'Now insert (and tap) your Security Key.', 'two-factor' ); ?></p>
<input type="hidden" name="u2f_response" id="u2f_response" />
<?php
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
$requests = get_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, true );
$response = json_decode( stripslashes( $_REQUEST['u2f_response'] ) );
$keys = self::get_security_keys( $user->ID );
try {
$reg = self::$u2f->doAuthenticate( $requests, $keys, $response );
$reg->last_used = time();
self::update_security_key( $user->ID, $reg );
return true;
} catch ( Exception $e ) {
return false;
}
}
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function is_available_for_user( $user ) {
return (bool) self::get_security_keys( $user->ID );
}
/**
* Inserts markup at the end of the user profile field for this provider.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function user_options( $user ) {
?>
<p>
<?php esc_html_e( 'Requires an HTTPS connection. Configure your security keys in the "Security Keys" section below.', 'two-factor' ); ?>
</p>
<?php
}
/**
* Add registered security key to a user.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param object $register The data of registered security key.
* @return int|bool Meta ID on success, false on failure.
*/
public static function add_security_key( $user_id, $register ) {
if ( ! is_numeric( $user_id ) ) {
return false;
}
if (
! is_object( $register )
|| ! property_exists( $register, 'keyHandle' ) || empty( $register->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $register, 'certificate' ) || empty( $register->certificate )
|| ! property_exists( $register, 'counter' ) || ( -1 > $register->counter )
) {
return false;
}
$register = array(
'keyHandle' => $register->keyHandle, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'publicKey' => $register->publicKey, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'certificate' => $register->certificate,
'counter' => $register->counter,
);
$register['name'] = __( 'New Security Key', 'two-factor' );
$register['added'] = time();
$register['last_used'] = $register['added'];
return add_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, $register );
}
/**
* Retrieve registered security keys for a user.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @return array|bool Array of keys on success, false on failure.
*/
public static function get_security_keys( $user_id ) {
if ( ! is_numeric( $user_id ) ) {
return false;
}
$keys = get_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY );
if ( $keys ) {
foreach ( $keys as &$key ) {
$key = (object) $key;
}
unset( $key );
}
return $keys;
}
/**
* Update registered security key.
*
* Use the $prev_value parameter to differentiate between meta fields with the
* same key and user ID.
*
* If the meta field for the user does not exist, it will be added.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param object $data The data of registered security key.
* @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure.
*/
public static function update_security_key( $user_id, $data ) {
if ( ! is_numeric( $user_id ) ) {
return false;
}
if (
! is_object( $data )
|| ! property_exists( $data, 'keyHandle' ) || empty( $data->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $data, 'certificate' ) || empty( $data->certificate )
|| ! property_exists( $data, 'counter' ) || ( -1 > $data->counter )
) {
return false;
}
$keys = self::get_security_keys( $user_id );
if ( $keys ) {
foreach ( $keys as $key ) {
if ( $key->keyHandle === $data->keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
return update_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, (array) $data, (array) $key );
}
}
}
return self::add_security_key( $user_id, $data );
}
/**
* Remove registered security key matching criteria from a user.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param string $keyHandle Optional. Key handle.
* @return bool True on success, false on failure.
*/
public static function delete_security_key( $user_id, $keyHandle = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
global $wpdb;
if ( ! is_numeric( $user_id ) ) {
return false;
}
$user_id = absint( $user_id );
if ( ! $user_id ) {
return false;
}
$keyHandle = wp_unslash( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$keyHandle = maybe_serialize( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$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 );
if ( $keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$key_handle_lookup = sprintf( ':"%s";s:', $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$query .= $wpdb->prepare(
' AND meta_value LIKE %s',
'%' . $wpdb->esc_like( $key_handle_lookup ) . '%'
);
}
$meta_ids = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( ! count( $meta_ids ) ) {
return false;
}
foreach ( $meta_ids as $meta_id ) {
delete_metadata_by_mid( 'user', $meta_id );
}
return true;
}
}