PluginProbe
Google Authenticator / 0.55
Google Authenticator v0.55
trunk 0.20 0.30 0.35 0.36 0.37 0.38 0.39 0.40 0.41 0.42 0.43 0.44 0.45 0.46 0.47 0.48 0.50 0.51 0.52 0.53 0.54 0.55 0.56
google-authenticator / google-authenticator.php

google-authenticator.php in Google Authenticator 0.55, at google-authenticator.php

1,022 lines 40.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: Google Authenticator
4 Plugin URI: https://github.com/ivankruchkoff/google-authenticator
5 Description: Two-Factor Authentication for WordPress using the Android/iPhone/Blackberry app as One Time Password generator.
6 Author: Ivan Kruchkoff
7 Version: 0.55
8 Author URI: https://github.com/ivankruchkoff
9 Compatibility: WordPress 5.6
10 Text Domain: google-authenticator
11 Domain Path: /lang
12
13 ----------------------------------------------------------------------------
14
15
16 Thanks to Paweł Nowacki for the Polish translation.
17 Thanks to Fabio Zumbi for the Portuguese translation.
18 Thanks to Guido Schalkx for the Dutch translation.
19 Thanks to Henrik Schack for creating / maintaining versions 0.20 to 0.48
20 Thanks to Ivan Kruchkoff for his UX improvements in user signup.
21 Thanks to Bryan Ruiz for his Base32 encode/decode class, found at php.net.
22 Thanks to Tobias Bäthge for his major code rewrite and German translation.
23 Thanks to Pascal de Bruijn for his relaxed mode idea.
24 Thanks to Daniel Werl for his usability tips.
25 Thanks to Dion Hulse for his bugfixes.
26 Thanks to Aldo Latino for his Italian translation.
27 Thanks to Kaijia Feng for his Simplified Chinese translation.
28 Thanks to Ian Dunn for fixing some depricated function calls.
29 Thanks to Kimmo Suominen for fixing the iPhone description issue.
30 Thanks to Alex Concha for some security tips.
31 Thanks to Sébastien Prunier for his Spanish and French translations.
32
33 ----------------------------------------------------------------------------
34
35 Versions from 0.49 onwards
36 Copyright 2019 Ivan Kruchkoff
37
38 Versions up to and including 0.48
39 Copyright 2013 Henrik Schack (email : henrik@schack.dk)
40
41 This program is free software; you can redistribute it and/or modify
42 it under the terms of the GNU General Public License as published by
43 the Free Software Foundation; either version 2 of the License, or
44 (at your option) any later version.
45
46 This program is distributed in the hope that it will be useful,
47 but WITHOUT ANY WARRANTY; without even the implied warranty of
48 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
49 GNU General Public License for more details.
50
51 You should have received a copy of the GNU General Public License
52 along with this program; if not, write to the Free Software
53 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
54 */
55
56 class GoogleAuthenticator {
57
58 static $instance; // to store a reference to the plugin, allows other plugins to remove actions
59 const SETUP_PAGE = 'google_authenticator_user_page';
60 protected $error_message = null;
61
62 /**
63 * Constructor, entry point of the plugin
64 */
65 function __construct() {
66 self::$instance = $this;
67 add_action( 'init', array( $this, 'init' ) );
68 }
69
70 /**
71 * Initialization, Hooks, and localization
72 */
73 function init() {
74 if ( ! class_exists( 'Base32' ) ) {
75 require_once( 'base32.php' );
76 }
77
78 if ( ! $this->is_two_screen_signin_enabled() ) {
79 add_action( 'login_form', array( $this, 'loginform' ) );
80 add_action( 'login_footer', array( $this, 'loginfooter' ) );
81 }
82
83 add_filter( 'authenticate', array( $this, 'check_otp' ), 50, 3 );
84
85 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
86 add_action( 'wp_ajax_GoogleAuthenticator_action', array( $this, 'ajax_callback' ) );
87 }
88
89 add_action( 'personal_options_update', array( $this, 'personal_options_update' ) );
90 add_action( 'profile_personal_options', array( $this, 'profile_personal_options' ) );
91 add_action( 'edit_user_profile', array( $this, 'edit_user_profile' ) );
92 add_action( 'edit_user_profile_update', array( $this, 'edit_user_profile_update' ) );
93
94 add_action( 'admin_enqueue_scripts', array( $this, 'add_qrcode_script' ) );
95 add_action( 'admin_menu', array ( $this, 'add_pages' ) );
96 add_action( 'network_admin_menu', array ( $this, 'add_pages' ) );
97 add_action( 'current_screen', array ( $this, 'redirect_if_setup_required' ) );
98 add_action( 'admin_notices', array ( $this, 'successful_signup_message' ) );
99 add_action( 'load-admin_page_google_authenticator_user_page', array( $this, 'save_submitted_setup_page' ) );
100
101 load_plugin_textdomain( 'google-authenticator', false, basename( dirname( __FILE__ ) ) . '/lang' );
102 }
103
104 /**
105 * Whether we show Google Auth code on the login screen, or after the user has entered their username and password.
106 *
107 * If it's on a separate screen, it means username / passwords can still be bruteforced, but logins can't occur without 2fa
108 *
109 * @return bool
110 */
111 function is_two_screen_signin_enabled() {
112 $two_screen_mfa = is_multisite() ? get_site_option( 'googleauthenticator_two_screen_signin' ) : get_option( 'googleauthenticator_two_screen_signin' );
113 return !! $two_screen_mfa;
114 }
115
116 /**
117 * Check the verification code entered by the user.
118 */
119
120 function verify( $secretkey, $thistry, $relaxedmode, $lasttimeslot ) {
121 // Did the user enter 6 digits ?
122 if ( strlen( $thistry ) != 6) {
123 return false;
124 } else {
125 $thistry = intval ( $thistry );
126 }
127 // If user is running in relaxed mode, we allow more time drifting
128 // ±4 min, as opposed to ± 30 seconds in normal mode.
129 if ( $relaxedmode == 'enabled' ) {
130 $firstcount = -8;
131 $lastcount = 8;
132 } else {
133 $firstcount = -1;
134 $lastcount = 1;
135 }
136
137 $tm = floor( time() / 30 );
138
139 $secretkey = Base32::decode( $secretkey );
140 if ( ! is_string( $secretkey ) || '' === $secretkey ) {
141 return false;
142 }
143 // Keys from 30 seconds before and after are valid aswell.
144 for ($i=$firstcount; $i<=$lastcount; $i++) {
145 // Pack time into binary string
146 $time=chr(0).chr(0).chr(0).chr(0).pack('N*',$tm+$i);
147 // Hash it with users secret key
148 $hm = hash_hmac( 'SHA1', $time, $secretkey, true );
149 // Use last nipple of result as index/offset
150 $offset = ord(substr($hm,-1)) & 0x0F;
151 // grab 4 bytes of the result
152 $hashpart=substr($hm,$offset,4);
153 // Unpak binary value
154 $value=unpack("N",$hashpart);
155 $value=$value[1];
156 // Only 32 bits
157 $value = $value & 0x7FFFFFFF;
158 $value = $value % 1000000;
159 if ( $value === $thistry ) {
160 // Check for replay (Man-in-the-middle) attack.
161 // Since this is not Star Trek, time can only move forward,
162 // meaning current login attempt has to be in the future compared to
163 // last successful login.
164 if ( $lasttimeslot >= ($tm+$i) ) {
165 error_log("Google Authenticator plugin: Man-in-the-middle attack detected (Could also be 2 legit login attempts within the same 30 second period)");
166 return false;
167 }
168 // Return timeslot in which login happened.
169 return $tm+$i;
170 }
171 }
172 return false;
173 }
174
175 /**
176 * Create a new random secret for the Google Authenticator app.
177 * 16 characters, randomly chosen from the allowed Base32 characters
178 * equals 10 bytes = 80 bits, as 256^10 = 32^16 = 2^80
179 */
180 function create_secret() {
181 $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // allowed characters in Base32
182 $secret = '';
183 for ( $i = 0; $i < 16; $i++ ) {
184 $secret .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
185 }
186 return $secret;
187 }
188
189 /**
190 * Add the script to generate QR codes.
191 */
192 function add_qrcode_script() {
193 wp_enqueue_script('jquery');
194 wp_register_script('qrcode_script', plugins_url('jquery.qrcode.min.js', __FILE__),array("jquery"));
195 wp_enqueue_script('qrcode_script');
196 }
197
198 /**
199 * Add 2fa pages to menus
200 */
201 function add_pages() {
202 // No menu entry for this page
203 add_submenu_page( '', esc_html__( 'Google Authenticator', 'google-authenticator' ), null, 'read', self::SETUP_PAGE, array( $this, 'user_setup_page' ) );
204
205 // Site admin screen
206 add_submenu_page( 'options-general.php', esc_html__( 'Google Authenticator', 'google-authenticator' ), esc_html__( 'Google Authenticator', 'google-authenticator' ), 'manage_options', 'google_authenticator', array( $this, 'admin_setup_page' ) );
207
208 // Network admin screen
209 add_submenu_page( 'settings.php', esc_html__( 'Google Authenticator', 'google-authenticator' ), esc_html__( 'Google Authenticator', 'google-authenticator' ), 'manage_network_options', 'google_authenticator', array( $this, 'network_admin_setup_page' ) );
210 }
211
212 /**
213 * Determine if a user needs to setup authy 2fa
214 * @return bool
215 */
216 function user_needs_to_setup_google_authenticator() {
217 $user = wp_get_current_user();
218 $enabled = trim(get_user_option( 'googleauthenticator_enabled', $user->ID ) ) === 'enabled';
219 if ( $enabled ) {
220 return false;
221 }
222
223 $must_signup = false;
224 $user_role = ! empty( $user->roles ) ? $user->roles[0] : '';
225 $check_single_site_admin_options = true;
226
227 if ( is_multisite() ) {
228 $roles = get_site_option( 'googleauthenticator_mandatory_mfa_roles', array() );
229 if ( in_array( $user_role, $roles, true ) ) {
230 $must_signup = true;
231 }
232 $check_single_site_admin_options = '1' !== get_site_option( 'googleauthenticator_network_only' ) ;
233 }
234
235 if ( ! $must_signup && $check_single_site_admin_options ) {
236 $roles = get_option( 'googleauthenticator_mandatory_mfa_roles', array() );
237 if ( in_array( $user_role, $roles, true ) ) {
238 $must_signup = true;
239 }
240
241 }
242
243 return apply_filters( 'google_authenticator_needs_setup', $must_signup, $user );
244 }
245
246 /**
247 * Send users to the signup page if they must signup.
248 */
249 function redirect_if_setup_required() {
250 if ( $this->user_needs_to_setup_google_authenticator() ) {
251 $screen = get_current_screen();
252 $pagename = 'admin_page_' . self::SETUP_PAGE;
253 if ( is_a( $screen, 'WP_Screen') && in_array( $screen->id, array( $pagename, 'profile' ) ) ) {
254 return;
255 }
256
257 // Some check against super admin so they can enable/disable the plugin.
258 $location = admin_url( 'admin.php?page=' . self::SETUP_PAGE );
259 wp_redirect( $location);
260 exit;
261 }
262 }
263
264 /**
265 * Save the GA secret if valid totp is provided
266 * @return void
267 */
268 function save_submitted_setup_page() {
269 $this->error_message = null; // Reset a previous error message if it was set
270 $user = wp_get_current_user();
271 $secret = empty( $_POST['GA_secret'] ) ? '' : sanitize_text_field( $_POST['GA_secret'] );
272 $otp = empty( $_POST['GA_otp_code'] ) ? '' : sanitize_text_field( $_POST['GA_otp_code'] );
273 if ( '' === $secret || '' === $otp ) {
274 return;
275 }
276 $relaxed_mode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user->ID ) );
277 $relaxed_mode = 'enabled' === $relaxed_mode ? 'enabled' : 'disabled';
278 if ( $timeslot = $this->verify( $secret, $otp, $relaxed_mode, '' ) ) {
279 update_user_option( $user->ID, 'googleauthenticator_lasttimeslot', $timeslot, true );
280 update_user_option( $user->ID, 'googleauthenticator_secret', $secret, true );
281 update_user_option( $user->ID, 'googleauthenticator_enabled', 'enabled', true );
282 $location = admin_url( 'index.php?googleauthenticator=enabled' );
283 wp_redirect( $location );
284 exit;
285 };
286
287 $this->error_message = new WP_Error( 'invalid-otp', esc_html__( "OTP code doesn't match supplied secret, please check you've configured Authenticator correctly.", 'google-authenticator' ) );
288 }
289
290 /**
291 * Show the user a success message after we redirect them following successful google authenticator setup
292 */
293 function successful_signup_message() {
294 if ( ! empty( $_GET['googleauthenticator'] ) && 'enabled' === $_GET['googleauthenticator'] ) : ?>
295 <div class="updated notice">
296 <p><?php esc_html_e( 'Congratulations, you have successfully enabled Google Authenticator for your account', 'google-authenticator' ); ?></p>
297 </div>
298
299 <?php endif;
300 }
301
302 /**
303 * Callback function to render the google authenticator setup page
304 */
305 function user_setup_page() {
306 $user = wp_get_current_user();
307 $enabled = trim(get_user_option( 'googleauthenticator_enabled', $user->ID ) ) === 'enabled';
308 if ( $enabled ) {
309 $location = admin_url( 'index.php' );
310 wp_redirect( $location );
311 exit;
312 }
313 $error = $this->error_message;
314
315 $app_links = array(
316 array(
317 'text' => __( 'iOS: Authy', 'google-authenticator' ),
318 'link' => 'https://itunes.apple.com/app/authy/id494168017',
319 ),
320 array(
321 'text' => __( 'iOS: Google Authenticator', 'google-authenticator' ),
322 'link' => 'https://itunes.apple.com/app/google-authenticator/id388497605',
323 ),
324 array(
325 'text' => __( 'Android: Authy', 'google-authenticator' ),
326 'link' => 'https://play.google.com/store/apps/details?id=com.authy.authy',
327 ),
328 array(
329 'text' => __( 'Android: Google Authenticator', 'google-authenticator' ),
330 'link' => 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2',
331 ),
332 array(
333 'text' => __( 'Windows Phone', 'google-authenticator' ),
334 'link' => 'https://www.microsoft.com/store/p/authenticator/9nblggh08h54',
335 ),
336 array(
337 'text' => __( 'Chrome Browser', 'google-authenticator' ),
338 'link' => 'https://chrome.google.com/webstore/detail/authy-chrome-extension/fhgenkpocbhhddlgkjnfghpjanffonno',
339 ),
340 array(
341 'text' => __( 'Desktop', 'google-authenticator' ),
342 'link' => 'https://authy.com/download/',
343 ),
344
345 );
346
347 ?>
348 <div class="wrap">
349 <h1><?php esc_html_e( 'Google Authenticator Settings', 'google-authenticator' ); ?></h1>
350 <?php if (is_wp_error( $error ) ): ?>
351 <div class="error notice"><p><?php esc_html_e( $error->get_error_message() ); ?></p></div>
352 <?php endif; ?>
353 <p><?php echo esc_html__( "If you haven't already done so, please install the Authy or Google Authenticator app on your mobile device from the App Store:", 'google-authenticator' ); ?></p>
354 <ul>
355 <?php foreach( $app_links as $app_link ): ?>
356 <li><a href="<?php echo esc_url( $app_link[ 'link' ] ); ?>"><?php echo esc_html( $app_link[ 'text' ] ); ?></a></li>
357 <?php endforeach; ?>
358 </ul>
359 <p><?php echo esc_html__( 'The easiest way to enable your account is to add an account by scanning the QR code using the app.', 'google-authenticator' ); ?></p>
360 <p>
361 <?php echo esc_html__( "An account can also be added by typing in the secret. After you've added your account to the App, please type the code you see on the screen into the Authenticator Code field and press the Verify Authenticator Code button.", 'google-authenticator' ); ?>
362 </p>
363 <p>
364 <?php echo esc_html__( 'If the account setup was successful, you will be logged out, and will need to login again using your Username, Password and Authenticator code generated using the App on your mobile device.', 'google-authenticator' ); ?>
365 </p>
366 <form method="post">
367 <?php $this->profile_personal_options( array(
368 'show_active' => false,
369 'show_relaxed_mode' => false,
370 'show_description' => false,
371 'show_secret_qr' => true,
372 'show_secret_buttons' => false,
373 'show_authenticator_code' => true,
374 'show_app_password' => false,
375 )); ?>
376 </form>
377 </div>
378 <?php
379 }
380
381 /**
382 * Save site / network wide settings
383 * @param $is_network
384 */
385 function save_submitted_admin_setup_page( $is_network ) {
386 $nonce = isset( $_POST['googleauthenticator'] )
387 ? sanitize_text_field( wp_unslash( $_POST['googleauthenticator'] ) )
388 : '';
389 if ( wp_verify_nonce( $nonce, 'googleauthenticator' ) ) {
390 if ( $is_network ) {
391 $network_settings_only = array_key_exists( 'network_settings_only', $_POST );
392 if ( current_user_can( 'manage_network_options' ) ) {
393 update_site_option( 'googleauthenticator_network_only', $network_settings_only );
394 }
395 }
396 $two_screen_mfa = array_key_exists( 'two_screen_approach', $_POST ) && 'true' === $_POST[ 'two_screen_approach' ];
397 if ( is_multisite() && $is_network ) {
398 if ( current_user_can( 'manage_network_options' ) ) {
399 update_site_option( 'googleauthenticator_two_screen_signin', $two_screen_mfa );
400 }
401 } elseif ( ! $is_network ) {
402 if ( current_user_can( 'manage_options' ) ) {
403 update_option( 'googleauthenticator_two_screen_signin', $two_screen_mfa );
404 }
405 }
406 $roles = isset( $_POST['roles'] ) ? (array) $_POST['roles'] : array();
407 $roles = array_map( 'sanitize_text_field', $roles );
408
409 if ( $is_network ) {
410 if ( current_user_can( 'manage_network_options' ) ) {
411 update_site_option( 'googleauthenticator_mandatory_mfa_roles', $roles );
412 }
413 } else {
414 if ( current_user_can( 'manage_options' ) ) {
415 update_option( 'googleauthenticator_mandatory_mfa_roles', $roles );
416 }
417 }
418 return true;
419 }
420 return false;
421 }
422
423 /**
424 * Callback function to render the google authenticator setup page
425 */
426 function common_admin_setup_page( $is_network = false ) {
427 if ( $is_network ) {
428 $site_ids = get_sites( 'fields=ids' );
429 $roles = get_editable_roles();
430 foreach( $site_ids as $site_id ) {
431 switch_to_blog( $site_id );
432 $roles = array_merge( $roles, get_editable_roles() );
433 restore_current_blog();
434 }
435 $edit_enabled = true;
436 } else {
437 $roles = get_editable_roles();
438 $edit_enabled = is_multisite() ? boolval( get_site_option( 'googleauthenticator_network_only') ) : true;
439 }
440 $is_updated = $this->save_submitted_admin_setup_page( $is_network );
441 ?>
442 <div class="wrap">
443 <h1><?php esc_html_e( 'Google Authenticator Settings', 'google-authenticator' ); ?></h1>
444 <?php if ( $is_updated ): ?>
445 <?php if ( $is_network ): ?>
446 <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Successfullly saved your settings for the network', 'google-authenticator' ); ?></p></div>
447 <?php else: ?>
448 <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Successfullly saved your settings for the site', 'google-authenticator' ); ?></p></div>
449 <?php endif; ?>
450 <?php endif; ?>
451 <form method="post">
452 <?php if ( $is_network ): ?>
453 <h2><?php esc_html_e( 'Network Settings', 'google-authenticator' ); ?></h2>
454 <p>
455 <label>
456 <input name="network_settings_only" type="checkbox" value="true" <?php checked( get_site_option( 'googleauthenticator_network_only' ) ); ?>>
457 <?php esc_html_e( 'Only use network-wide settings, ignoring site settings.', 'google-authenticator' ); ?>
458 </label>
459 </p>
460 <?php endif; ?>
461 <?php if ( is_multisite() && $is_network || ! is_multisite() ): ?>
462 <?php $two_screen_mfa = is_multisite() ? get_site_option( 'googleauthenticator_two_screen_signin' ) : get_option( 'googleauthenticator_two_screen_signin' ); ?>
463 <h2><?php esc_html_e( 'Two Screen Signin', 'google-authenticator' ); ?></h2>
464 <p>
465 <label>
466 <input name="two_screen_approach" type="checkbox" value="true" <?php checked( $two_screen_mfa ); ?>>
467 <?php esc_html_e( 'Ask for authenticator code on secondary login screen', 'google-authenticator' ); ?>
468 </label>
469 </p>
470 <?php endif; ?>
471 <h2><?php esc_html_e( 'Roles requiring Google Authenticator Enabled', 'google-authenticator' ); ?></h2>
472 <?php foreach ($roles as $role_key => $role) {
473 $this->show_role_checkbox( $role_key, $role, $is_network );
474 }
475 if ( $edit_enabled ) {
476 wp_nonce_field( 'googleauthenticator', 'googleauthenticator' );
477 submit_button();
478 } else {
479 esc_html_e( 'Network-wide settings in effect, only a super admin can modify them.', 'google-authenticator' );
480 if ( current_user_can( 'manage_network' ) ) :?>
481 <a href="<?php echo network_admin_url( 'settings.php?page=google_authenticator' ) ?>"><?php esc_html_e( 'Change network wide Google Authenticator settings', 'google-authenticator' ); ?></a>
482 <?php endif;
483 }
484 ?>
485
486 </form>
487 </div>
488 <?php
489 }
490
491 /**
492 * Render a checkbox for a role
493 * @param $role_key
494 * @param $role
495 * @param $is_network
496 */
497 function show_role_checkbox( $role_key, $role, $is_network ) {
498 $network_roles = get_site_option( 'googleauthenticator_mandatory_mfa_roles', array() );
499 $network_only = is_multisite() && boolval( get_site_option( 'googleauthenticator_network_only' ) );
500 $roles = get_option( 'googleauthenticator_mandatory_mfa_roles', array() );
501 if ( $network_only ) {
502 $checked = in_array( $role_key, $network_roles );
503 } else {
504 $checked = in_array( $role_key, array_merge( $roles, $network_roles ) );
505 }
506
507 /**
508 * Criteria under which permission field can be readonly.
509 * 1. Site must be a multisite AND
510 * Either
511 * a. googleauthenticator_network_only network option is set via /wp-admin/network/settings.php?page=google_authenticator
512 *
513 * OR
514 * b. the network option for this role is set via /wp-admin/network/settings.php?page=google_authenticator
515 */
516 $readonly = is_multisite() && ( ( ! $is_network && $network_only ) || ( ! $is_network && in_array( $role_key, $network_roles ) && ! in_array( $role_key, $roles ) ) );
517 $readonly_label = '';
518
519 if ( $readonly ) {
520 if ( current_user_can( 'manage_network' ) ) {
521 $readonly_label = __( "Sorry, you can't disable checks for this role as it's enabled at the network level.", 'google-authenticator' );
522 } else {
523 $readonly_label = sprintf( __( 'Sorry, this role is enabled at the network level and can only be disabled via the <a href="%s">network settings</a>', 'google-authenticator' ), network_admin_url( 'settings.php?page=google_authenticator' ) );
524 }
525 }
526
527 $readonly = $readonly ? ' readonly="readonly"' : '';
528 ?>
529 <p><label><input name="roles[]" type="checkbox"<?php echo esc_html( $readonly ) . checked( $checked, true, false ); ?>value="<?php esc_attr_e( $role_key ); ?>"><strong><?php esc_html_e( $role[ 'name' ] ); ?></strong></label> <?php echo $readonly_label; ?></p>
530 <?php
531 }
532
533 /**
534 * Admin setup screen
535 */
536 function admin_setup_page() {
537 $this->common_admin_setup_page();
538
539 }
540
541 /**
542 * Network admin setup screen
543 */
544 function network_admin_setup_page() {
545 $this->common_admin_setup_page( true );
546 }
547 /**
548 * Add verification code field to login form.
549 */
550 function loginform() {
551 echo "\t<p>\n";
552 echo "\t\t<label title=\"".__('If you don\'t have Google Authenticator enabled for your WordPress account, leave this field empty.','google-authenticator')."\">".__('Google Authenticator code','google-authenticator')."<span id=\"google-auth-info\"></span><br />\n";
553 echo "\t\t<input type=\"text\" name=\"googleotp\" id=\"googleotp\" class=\"input\" value=\"\" size=\"20\" style=\"ime-mode: inactive;\" autocomplete=\"off\" /></label>\n";
554 echo "\t</p>\n";
555 echo "\t<script type=\"text/javascript\">\n";
556 echo "\t\tdocument.getElementById(\"googleotp\").focus();\n";
557 echo "\t</script>\n";
558 }
559
560 /**
561 * Disable autocomplete on Google Authenticator code input field.
562 */
563 function loginfooter() {
564 echo "\n<script type=\"text/javascript\">\n";
565 echo "\ttry{\n";
566 echo "\t\tdocument.getElementById('user_email').setAttribute('autocomplete','off');\n";
567 echo "\t} catch(e){}\n";
568 echo "</script>\n";
569 }
570
571 /**
572 * Login form handling.
573 * Check Google Authenticator verification code, if user has been setup to do so.
574 * @param wordpressuser / WP_Error
575 * @return user/loginstatus
576 */
577 function check_otp( $user, $username = '', $password = '' ) {
578 // Store result of loginprocess, so far.
579 $userstate = $user;
580
581 // Get information on user, we need this in case an app password has been enabled,
582 // since the $user var only contain an error at this point in the login flow.
583 if ( get_user_by( 'email', $username ) === false ) {
584 $user = get_user_by( 'login', $username );
585 } else {
586 $user = get_user_by( 'email', $username );
587 }
588
589 // Does the user have the Google Authenticator enabled ?
590 if ( isset( $user->ID ) && trim(get_user_option( 'googleauthenticator_enabled', $user->ID ) ) == 'enabled' ) {
591
592 // Get the users secret
593 $GA_secret = trim( get_user_option( 'googleauthenticator_secret', $user->ID ) );
594
595 // Figure out if user is using relaxed mode ?
596 $GA_relaxedmode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user->ID ) );
597
598 // Get the verification code entered by the user trying to login
599 if ( !empty( $_POST['googleotp'] )) { // Prevent PHP notices when using app password login
600 $otp = trim( $_POST[ 'googleotp' ] );
601 } else {
602 $otp = '';
603 }
604 // When was the last successful login performed ?
605 $lasttimeslot = trim( get_user_option( 'googleauthenticator_lasttimeslot', $user->ID ) );
606 // Valid code ?
607 if ( $timeslot = $this->verify( $GA_secret, $otp, $GA_relaxedmode, $lasttimeslot ) ) {
608 // Store the timeslot in which login was successful.
609 update_user_option( $user->ID, 'googleauthenticator_lasttimeslot', $timeslot, true );
610 return $userstate;
611 } else {
612 // No, lets see if an app password is enabled, and this is an XMLRPC / APP login ?
613 if ( trim( get_user_option( 'googleauthenticator_pwdenabled', $user->ID ) ) == 'enabled' && ( defined('XMLRPC_REQUEST') || defined('APP_REQUEST') ) ) {
614 $GA_passwords = json_decode( get_user_option( 'googleauthenticator_passwords', $user->ID ) );
615 if ( ! is_object( $GA_passwords ) || ! isset( $GA_passwords->password ) ) {
616 return new WP_Error( 'invalid_google_authenticator_password', __( '<strong>ERROR</strong>: The Google Authenticator password is incorrect.', 'google-authenticator' ) );
617 }
618 $passwordhash = trim( $GA_passwords->password );
619 $usersha1 = sha1( strtoupper( str_replace( ' ', '', $password ) ) );
620 if ( $passwordhash == $usersha1 ) { // ToDo: Remove after some time when users have migrated to new format
621 return new WP_User( $user->ID );
622 // Try the new version based on thee wp_hash_password function
623 } elseif (wp_check_password( strtoupper( str_replace( ' ', '', $password ) ), $passwordhash)) {
624 return new WP_User( $user->ID );
625 } else {
626 // Wrong XMLRPC/APP password !
627 return new WP_Error( 'invalid_google_authenticator_password', __( '<strong>ERROR</strong>: The Google Authenticator password is incorrect.', 'google-authenticator' ) );
628 }
629 } else {
630 if ( ! $this->is_two_screen_signin_enabled() ) {
631 return new WP_Error( 'invalid_google_authenticator_token', __( '<strong>ERROR</strong>: The Google Authenticator code is incorrect or has expired.', 'google-authenticator' ) );
632 } else {
633 wp_logout();
634 $this->secondary_login_screen();
635 exit;
636 }
637 }
638 }
639 }
640 // Google Authenticator isn't enabled for this account,
641 // just resume normal authentication.
642 return $userstate;
643 }
644
645 function secondary_login_screen() {
646 $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : admin_url();
647 login_header( esc_html__('Secondary Login Screen', 'google-authenticator' ) );
648 if ( array_key_exists( 'googleotp', $_REQUEST ) ) {
649 if ( 0 === strlen( $_REQUEST[ 'googleotp'] ) ) {
650 $error_message = __( '<strong>ERROR</strong>: The Google Authenticator code is missing.', 'google-authenticator' );
651 } else {
652 $error_message = __( '<strong>ERROR</strong>: The Google Authenticator code is incorrect or has expired.', 'google-authenticator' );
653 }
654 echo '<div id="login_error">' . $error_message . '</div>';
655 }?>
656 <form name="loginform" id="loginform" action="<?php echo esc_url( site_url( 'wp-login.php', 'login_post' ) ); ?>" method="post">
657 <input type="hidden" name="log" value="<?php echo esc_attr( isset( $_REQUEST['log'] ) ? $_REQUEST['log'] : '' ); ?>" />
658 <input type="hidden" name="pwd" value="<?php echo esc_attr( isset( $_REQUEST['pwd'] ) ? $_REQUEST['pwd'] : '' ); ?>" />
659 <input type="hidden" name="wp-submit" value="<?php echo esc_attr( isset( $_REQUEST['wp-submit'] ) ? $_REQUEST['wp-submit'] : '' ); ?>" />
660 <?php if ( array_key_exists( 'rememberme', $_REQUEST ) && 'forever' === $_REQUEST[ 'rememberme']): ?>
661 <input name="rememberme" type="hidden" id="rememberme" value="forever" />
662 <?php endif; ?>
663 <?php $this->loginform(); ?>
664 <p><?php esc_html_e( 'Please enter the Google Authenticator code using the app on your device.', 'google-authenticator' ); ?></p>
665 <p class="submit">
666 <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e('Log In'); ?>" />
667 <input type="hidden" name="redirect_to" value="<?php echo esc_attr($redirect_to); ?>" />
668 <input type="hidden" name="testcookie" value="1" />
669 </p>
670 </form>
671 <?php
672 login_footer();
673 }
674
675
676 /**
677 * Extend personal profile page with Google Authenticator settings.
678 */
679 function profile_personal_options( $args = array() ) {
680 $defaults = array(
681 'show_active' => true,
682 'show_relaxed_mode' => true,
683 'show_description' => true,
684 'show_secret_qr' => false,
685 'show_secret_buttons' => true,
686 'show_authenticator_code' => false,
687 'show_app_password' => true,
688 );
689
690 $args = wp_parse_args( $args, $defaults );
691
692 $user = wp_get_current_user();
693 $user_id = $user->ID;
694
695 // If editing of Google Authenticator settings has been disabled, just return
696 $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) );
697 if ( $GA_hidefromuser == 'enabled') return;
698
699 $GA_secret = trim( get_user_option( 'googleauthenticator_secret', $user_id ) );
700 $GA_enabled = trim( get_user_option( 'googleauthenticator_enabled', $user_id ) );
701 $GA_relaxedmode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user_id ) );
702 $GA_description = trim( get_user_option( 'googleauthenticator_description', $user_id ) );
703 $GA_pwdenabled = trim( get_user_option( 'googleauthenticator_pwdenabled', $user_id ) );
704 $GA_password = trim( get_user_option( 'googleauthenticator_passwords', $user_id ) );
705
706 // We dont store the generated app password in cleartext so there is no point in trying
707 // to show the user anything except from the fact that a password exists.
708 if ( $GA_password != '' ) {
709 $GA_password = "XXXX XXXX XXXX XXXX";
710 }
711
712 // In case the user has no secret ready (new install), we create one. or use the one they just posted
713 if ( '' == $GA_secret ) {
714 $GA_secret = array_key_exists( 'GA_secret', $_REQUEST ) ? sanitize_text_field( $_REQUEST[ 'GA_secret' ] ) : $this->create_secret();
715 }
716
717 if ( '' == $GA_description ) {
718 // Super admins and users with accounts on more than one site get the network name as the helpful name,
719 // everyone else gets the site that they're on
720 if ( is_multisite() && ( 1 < count( get_blogs_of_user( $user_id ) ) || is_super_admin() ) ) {
721 $blog_details = get_blog_details( get_network()->id );
722 $GA_description = sanitize_text_field( $blog_details ? $blog_details->blogname : get_bloginfo( 'name' ) );
723 } else {
724 $GA_description = sanitize_text_field( get_bloginfo( 'name' ) );
725 }
726 }
727
728 echo "<h3>".__( 'Google Authenticator Settings', 'google-authenticator' )."</h3>\n";
729
730 echo "<table class=\"form-table\">\n";
731 echo "<tbody>\n";
732
733 if ( $args['show_active'] ) {
734 echo "<tr>\n";
735 echo "<th scope=\"row\">".__( 'Active', 'google-authenticator' )."</th>\n";
736 echo "<td>\n";
737 echo "<input name=\"GA_enabled\" id=\"GA_enabled\" class=\"tog\" type=\"checkbox\"" . checked( $GA_enabled, 'enabled', false ) . "/>\n";
738 echo "</td>\n";
739 echo "</tr>\n";
740 }
741
742 if ( $args['show_relaxed_mode'] ) {
743 echo "<tr>\n";
744 echo "<th scope=\"row\">" . __( 'Relaxed mode', 'google-authenticator' ) . "</th>\n";
745 echo "<td>\n";
746 echo "<input name=\"GA_relaxedmode\" id=\"GA_relaxedmode\" class=\"tog\" type=\"checkbox\"" . checked( $GA_relaxedmode, 'enabled', false ) . "/><span class=\"description\">" . __( ' Relaxed mode allows for more time drifting on your phone clock (&#177;4 min).', 'google-authenticator' ) . "</span>\n";
747 echo "</td>\n";
748 echo "</tr>\n";
749 }
750
751 $show_description_style = $args['show_description'] ? '' : 'display:none';
752 echo "<tr style=\"{$show_description_style}\">\n";
753 echo "<th><label for=\"GA_description\">" . esc_html__( 'Description', 'google-authenticator' ) . "</label></th>\n";
754 echo "<td><input name=\"GA_description\" id=\"GA_description\" value=\"" . esc_attr( $GA_description ) . "\" type=\"text\" size=\"25\" /><span class=\"description\">" . __( ' Description that you\'ll see in the Google Authenticator app on your phone.', 'google-authenticator' ) . "</span><br /></td>\n";
755 echo "</tr>\n";
756
757 echo "<tr>\n";
758 echo "<th><label for=\"GA_secret\">".__('Secret','google-authenticator')."</label></th>\n";
759 echo "<td>\n";
760 echo "<input name=\"GA_secret\" id=\"GA_secret\" value=\"" . esc_attr( $GA_secret) . "\" readonly=\"readonly\" type=\"text\" size=\"25\" />";
761 if ( $args['show_secret_buttons']) {
762 echo "<input name=\"GA_newsecret\" id=\"GA_newsecret\" value=\"".__("Create new secret",'google-authenticator')."\" type=\"button\" class=\"button\" />";
763 echo "<input name=\"show_qr\" id=\"show_qr\" value=\"".__("Show/Hide QR code",'google-authenticator')."\" type=\"button\" class=\"button\" onclick=\"ShowOrHideQRCode();\" />";
764 }
765 echo "</td>\n";
766 echo "</tr>\n";
767
768 echo "<tr>\n";
769 echo "<th></th>\n";
770
771 $qr_style = $args['show_secret_qr'] ? '' : 'display: none';
772 echo "<td><div id=\"GA_QR_INFO\" style=\"{$qr_style}\" >";
773 echo "<div id=\"GA_QRCODE\"/></div>";
774
775 echo '<span class="description"><br/> ' . __( 'Scan this with the Google Authenticator app.', 'google-authenticator' ) . '</span>';
776 echo "</div></td>\n";
777 echo "</tr>\n";
778 if ( $args['show_secret_qr']) : ?>
779 <script>
780 var qrcode="otpauth://totp/WordPress:"+escape(jQuery('#GA_description').val())+"?secret="+jQuery('#GA_secret').val()+"&issuer=WordPress";
781 jQuery('#GA_QRCODE').qrcode(qrcode);
782 </script>
783 <?php endif;
784
785 if ( $args['show_app_password']) {
786 echo "<tr>\n";
787 echo "<th scope=\"row\">".__( 'Enable App password', 'google-authenticator' )."</th>\n";
788 echo "<td>\n";
789 echo "<input name=\"GA_pwdenabled\" id=\"GA_pwdenabled\" class=\"tog\" type=\"checkbox\"" . checked( $GA_pwdenabled, 'enabled', false ) . "/><span class=\"description\">".__(' Enabling an App password will decrease your overall login security.','google-authenticator')."</span>\n";
790 echo "</td>\n";
791 echo "</tr>\n";
792
793 echo "<tr>\n";
794 echo "<th></th>\n";
795 echo "<td>\n";
796 echo "<input name=\"GA_password\" id=\"GA_password\" readonly=\"readonly\" value=\"".$GA_password."\" type=\"text\" size=\"25\" />";
797 echo "<input name=\"GA_createpassword\" id=\"GA_createpassword\" value=\"".__("Create new password",'google-authenticator')."\" type=\"button\" class=\"button\" />";
798 echo "<span class=\"description\" id=\"GA_passworddesc\"> ".__(' Password is not stored in cleartext, this is your only chance to see it.','google-authenticator')."</span>\n";
799 echo "</td>\n";
800 echo "</tr>\n";
801 }
802
803
804 if ( $args['show_authenticator_code']) {
805 echo "<tr>\n";
806 echo "<th><label for=\"GA_otp_code\">" . __( 'Authenticator Code', 'google-authenticator' ) . "</label></th>\n";
807 echo "<td><input name=\"GA_otp_code\" id=\"GA_otp_code\" type=\"text\" size=\"25\" /><span class=\"description\">" . __( 'After adding the site to your google authy account, add your authenticator code here.', 'google-authenticator' ) . "</span><br /></td>\n";
808 echo "</tr>\n";
809 }
810
811 echo "</tbody></table>\n";
812 if ( $args['show_authenticator_code']) {
813 submit_button( esc_html__( 'Verify Authenticator Code', 'google-authenticator' ) );
814 }
815 echo "<script type=\"text/javascript\">\n";
816 echo "var GAnonce='". esc_js( wp_create_nonce('GoogleAuthenticatoraction') )."';\n";
817
818 echo <<<ENDOFJS
819 //Create new secret and display it
820 jQuery('#GA_newsecret').bind('click', function() {
821 // Remove existing QRCode
822 jQuery('#GA_QRCODE').html("");
823 var data=new Object();
824 data['action'] = 'GoogleAuthenticator_action';
825 data['nonce'] = GAnonce;
826 jQuery.post(ajaxurl, data, function(response) {
827 jQuery('#GA_secret').val(response['new-secret']);
828 var qrcode="otpauth://totp/WordPress:"+escape(jQuery('#GA_description').val())+"?secret="+jQuery('#GA_secret').val()+"&issuer=WordPress";
829 jQuery('#GA_QRCODE').qrcode(qrcode);
830 jQuery('#GA_QR_INFO').show('slow');
831 });
832 });
833
834 // If the user starts modifying the description, hide the qrcode
835 jQuery('#GA_description').bind('focus blur change keyup', function() {
836 // Only remove QR Code if it's visible
837 if (jQuery('#GA_QR_INFO').is(':visible')) {
838 jQuery('#GA_QR_INFO').hide('slow');
839 jQuery('#GA_QRCODE').html("");
840 }
841 });
842
843 // Create new app password
844 jQuery('#GA_createpassword').bind('click',function() {
845 var data=new Object();
846 data['action'] = 'GoogleAuthenticator_action';
847 data['nonce'] = GAnonce;
848 data['save'] = 1;
849 jQuery.post(ajaxurl, data, function(response) {
850 jQuery('#GA_password').val(response['new-secret'].match(new RegExp(".{0,4}","g")).join(' '));
851 jQuery('#GA_passworddesc').show();
852 });
853 });
854
855 jQuery('#GA_enabled').bind('change',function() {
856 GoogleAuthenticator_apppasswordcontrol();
857 });
858
859 jQuery(document).ready(function() {
860 jQuery('#GA_passworddesc').hide();
861 GoogleAuthenticator_apppasswordcontrol();
862 });
863
864 function GoogleAuthenticator_apppasswordcontrol() {
865 if (jQuery('#GA_enabled').is(':checked')) {
866 jQuery('#GA_pwdenabled').removeAttr('disabled');
867 jQuery('#GA_createpassword').removeAttr('disabled');
868 } else {
869 jQuery('#GA_pwdenabled').removeAttr('checked')
870 jQuery('#GA_pwdenabled').attr('disabled', true);
871 jQuery('#GA_createpassword').attr('disabled', true);
872 }
873 }
874
875 function ShowOrHideQRCode() {
876 if (jQuery('#GA_QR_INFO').is(':hidden')) {
877 var qrcode="otpauth://totp/WordPress:"+escape(jQuery('#GA_description').val())+"?secret="+jQuery('#GA_secret').val()+"&issuer=WordPress";
878 jQuery('#GA_QRCODE').qrcode(qrcode);
879 jQuery('#GA_QR_INFO').show('slow');
880 } else {
881 jQuery('#GA_QR_INFO').hide('slow');
882 jQuery('#GA_QRCODE').html("");
883 }
884 }
885 </script>
886 ENDOFJS;
887 }
888
889 /**
890 * Form handling of Google Authenticator options added to personal profile page (user editing his own profile)
891 */
892 function personal_options_update() {
893 global $user_id;
894
895 // If editing of Google Authenticator settings has been disabled, just return
896 $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) );
897 if ( $GA_hidefromuser == 'enabled') return;
898
899
900 $GA_enabled = ! empty( $_POST['GA_enabled'] );
901 $GA_description = trim( sanitize_text_field( isset( $_POST['GA_description'] ) ? $_POST['GA_description'] : '' ) );
902 $GA_relaxedmode = ! empty( $_POST['GA_relaxedmode'] );
903 $GA_secret = sanitize_text_field( trim( isset( $_POST['GA_secret'] ) ? $_POST['GA_secret'] : '' ) );
904 $GA_pwdenabled = ! empty( $_POST['GA_pwdenabled'] );
905 $GA_password = str_replace( ' ', '', trim( isset( $_POST['GA_password'] ) ? $_POST['GA_password'] : '' ) );
906
907 if ( ! $GA_enabled ) {
908 $GA_enabled = 'disabled';
909 } else {
910 $GA_enabled = 'enabled';
911 }
912
913 if ( ! $GA_relaxedmode ) {
914 $GA_relaxedmode = 'disabled';
915 } else {
916 $GA_relaxedmode = 'enabled';
917 }
918
919
920 if ( ! $GA_pwdenabled ) {
921 $GA_pwdenabled = 'disabled';
922 } else {
923 $GA_pwdenabled = 'enabled';
924 }
925
926 // Only store password if a new one has been generated.
927 if (strtoupper($GA_password) != 'XXXXXXXXXXXXXXXX' ) {
928 // Store the password in a format that can be expanded easily later on if needed.
929 $GA_password = array( 'appname' => 'Default', 'password' => wp_hash_password( $GA_password ) );
930 update_user_option( $user_id, 'googleauthenticator_passwords', json_encode( $GA_password ), true );
931 }
932
933 update_user_option( $user_id, 'googleauthenticator_enabled', $GA_enabled, true );
934 update_user_option( $user_id, 'googleauthenticator_description', $GA_description, true );
935 update_user_option( $user_id, 'googleauthenticator_relaxedmode', $GA_relaxedmode, true );
936 update_user_option( $user_id, 'googleauthenticator_secret', $GA_secret, true );
937 update_user_option( $user_id, 'googleauthenticator_pwdenabled', $GA_pwdenabled, true );
938
939 }
940
941 /**
942 * Extend profile page with ability to enable/disable Google Authenticator authentication requirement.
943 * Used by an administrator when editing other users.
944 */
945 function edit_user_profile() {
946 global $user_id;
947 $GA_enabled = trim( get_user_option( 'googleauthenticator_enabled', $user_id ) );
948 $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) );
949 echo "<h3>".__('Google Authenticator Settings','google-authenticator')."</h3>\n";
950 echo "<table class=\"form-table\">\n";
951 echo "<tbody>\n";
952
953 echo "<tr>\n";
954 echo "<th scope=\"row\">".__('Hide settings from user','google-authenticator')."</th>\n";
955 echo "<td>\n";
956 echo "<div><input name=\"GA_hidefromuser\" id=\"GA_hidefromuser\" class=\"tog\" type=\"checkbox\"" . checked( $GA_hidefromuser, 'enabled', false ) . "/>\n";
957 echo "</td>\n";
958 echo "</tr>\n";
959
960 echo "<tr>\n";
961 echo "<th scope=\"row\">".__('Active','google-authenticator')."</th>\n";
962 echo "<td>\n";
963 echo "<div><input name=\"GA_enabled\" id=\"GA_enabled\" class=\"tog\" type=\"checkbox\"" . checked( $GA_enabled, 'enabled', false ) . "/>\n";
964 echo "</td>\n";
965 echo "</tr>\n";
966
967 echo "</tbody>\n";
968 echo "</table>\n";
969 }
970
971 /**
972 * Form handling of Google Authenticator options on edit profile page (admin user editing other user)
973 */
974 function edit_user_profile_update() {
975 global $user_id;
976
977 $GA_enabled = ! empty( $_POST['GA_enabled'] );
978 $GA_hidefromuser = ! empty( $_POST['GA_hidefromuser'] );
979
980 if ( ! $GA_enabled ) {
981 $GA_enabled = 'disabled';
982 } else {
983 $GA_enabled = 'enabled';
984 }
985
986 if ( ! $GA_hidefromuser ) {
987 $GA_hidefromuser = 'disabled';
988 } else {
989 $GA_hidefromuser = 'enabled';
990 }
991
992 update_user_option( $user_id, 'googleauthenticator_enabled', $GA_enabled, true );
993 update_user_option( $user_id, 'googleauthenticator_hidefromuser', $GA_hidefromuser, true );
994
995 }
996
997
998 /**
999 * AJAX callback function used to generate new secret
1000 */
1001 function ajax_callback() {
1002 global $user_id;
1003
1004 // Some AJAX security.
1005 check_ajax_referer( 'GoogleAuthenticatoraction', 'nonce' );
1006
1007 // Create new secret.
1008 $secret = $this->create_secret();
1009
1010 $result = array( 'new-secret' => $secret );
1011 header( 'Content-Type: application/json' );
1012 echo json_encode( $result );
1013
1014 // die() is required to return a proper result
1015 die();
1016 }
1017
1018 } // end class
1019
1020 $google_authenticator = new GoogleAuthenticator;
1021
1022