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

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