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

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