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

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