PluginProbe
Google Authenticator / 0.52
Google Authenticator v0.52
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.52, at google-authenticator.php

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