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

491 lines 18.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: http://henrik.schack.dk/google-authenticator-for-wordpress
5 Description: Two-Factor Authentication for WordPress using the Android/iPhone/Blackberry app as One Time Password generator.
6 Author: Henrik Schack
7 Version: 0.43
8 Author URI: http://henrik.schack.dk/
9 Compatibility: WordPress 3.4.1
10 Text Domain: google-authenticator
11 Domain Path: /lang
12
13 ----------------------------------------------------------------------------
14
15 Thanks to Bryan Ruiz for his Base32 encode/decode class, found at php.net.
16 Thanks to Tobias B�thge for his major code rewrite and German translation.
17 Thanks to Pascal de Bruijn for his relaxed mode idea.
18 Thanks to Daniel Werl for his usability tips.
19 Thanks to Dion Hulse for his bugfixes.
20 Thanks to Aldo Latino for his Italian translation.
21
22 ----------------------------------------------------------------------------
23
24 Copyright 2012 Henrik Schack (email : henrik@schack.dk)
25
26 This program is free software; you can redistribute it and/or modify
27 it under the terms of the GNU General Public License as published by
28 the Free Software Foundation; either version 2 of the License, or
29 (at your option) any later version.
30
31 This program is distributed in the hope that it will be useful,
32 but WITHOUT ANY WARRANTY; without even the implied warranty of
33 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34 GNU General Public License for more details.
35
36 You should have received a copy of the GNU General Public License
37 along with this program; if not, write to the Free Software
38 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
39 */
40
41 class GoogleAuthenticator {
42
43 static $instance; // to store a reference to the plugin, allows other plugins to remove actions
44
45 /**
46 * Constructor, entry point of the plugin
47 */
48 function __construct() {
49 self::$instance = $this;
50 add_action( 'init', array( $this, 'init' ) );
51 }
52
53 /**
54 * Initialization, Hooks, and localization
55 */
56 function init() {
57 require_once( 'base32.php' );
58
59 add_action( 'login_form', array( $this, 'loginform' ) );
60 add_action( 'login_footer', array( $this, 'loginfooter' ) );
61 add_filter( 'authenticate', array( $this, 'check_otp' ), 50, 3 );
62
63 if ( defined( 'DOING_AJAX' ) && DOING_AJAX )
64 add_action( 'wp_ajax_GoogleAuthenticator_action', array( $this, 'ajax_callback' ) );
65
66 add_action( 'personal_options_update', array( $this, 'personal_options_update' ) );
67 add_action( 'profile_personal_options', array( $this, 'profile_personal_options' ) );
68 add_action( 'edit_user_profile', array( $this, 'edit_user_profile' ) );
69 add_action( 'edit_user_profile_update', array( $this, 'edit_user_profile_update' ) );
70
71 load_plugin_textdomain( 'google-authenticator', false, basename( dirname( __FILE__ ) ) . '/lang' );
72 }
73
74 /**
75 * Check the verification code entered by the user.
76 */
77 function verify( $secretkey, $thistry, $relaxedmode ) {
78
79 // If user is running in relaxed mode, we allow more time drifting
80 // �4 min, as opposed to � 30 seconds in normal mode.
81 if ( $relaxedmode == 'enabled' ) {
82 $firstcount = -8;
83 $lastcount = 8;
84 } else {
85 $firstcount = -1;
86 $lastcount = 1;
87 }
88
89 $tm = floor( time() / 30 );
90
91 $secretkey=Base32::decode($secretkey);
92 // Keys from 30 seconds before and after are valid aswell.
93 for ($i=$firstcount; $i<=$lastcount; $i++) {
94 // Pack time into binary string
95 $time=chr(0).chr(0).chr(0).chr(0).pack('N*',$tm+$i);
96 // Hash it with users secret key
97 $hm = hash_hmac( 'SHA1', $time, $secretkey, true );
98 // Use last nipple of result as index/offset
99 $offset = ord(substr($hm,-1)) & 0x0F;
100 // grab 4 bytes of the result
101 $hashpart=substr($hm,$offset,4);
102 // Unpak binary value
103 $value=unpack("N",$hashpart);
104 $value=$value[1];
105 // Only 32 bits
106 $value = $value & 0x7FFFFFFF;
107 $value = $value % 1000000;
108 if ( $value == $thistry ) {
109 return true;
110 }
111 }
112 return false;
113 }
114
115 /**
116 * Create a new random secret for the Google Authenticator app.
117 * 16 characters, randomly chosen from the allowed Base32 characters
118 * equals 10 bytes = 80 bits, as 256^10 = 32^16 = 2^80
119 */
120 function create_secret() {
121 $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // allowed characters in Base32
122 $secret = '';
123 for ( $i = 0; $i < 16; $i++ ) {
124 $secret .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
125 }
126 return $secret;
127 }
128
129
130 /**
131 * Add verification code field to login form.
132 */
133 function loginform() {
134 echo "\t<p>\n";
135 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";
136 echo "\t\t<input type=\"text\" name=\"otp\" id=\"user_email\" class=\"input\" value=\"\" size=\"20\" tabindex=\"25\" /></label>\n";
137 echo "\t</p>\n";
138 }
139
140 /**
141 * Disable autocomplete on Google Authenticator code input field.
142 */
143 function loginfooter() {
144 echo "\n<script type=\"text/javascript\">\n";
145 echo "\ttry{\n";
146 echo "\t\tdocument.getElementById('user_email').setAttribute('autocomplete','off');\n";
147 echo "\t} catch(e){}\n";
148 echo "</script>\n";
149 }
150
151 /**
152 * Login form handling.
153 * Check Google Authenticator verification code, if user has been setup to do so.
154 * @param wordpressuser
155 * @return user/loginstatus
156 */
157 function check_otp( $user, $username = '', $password = '' ) {
158 // Store result of loginprocess, so far.
159 $userstate = $user;
160
161 // Get information on user, we need this in case an app password has been enabled,
162 // since the $user var only contain an error at this point in the login flow.
163 $user = get_userdatabylogin( $username );
164
165 // Does the user have the Google Authenticator enabled ?
166 if ( trim(get_user_option( 'googleauthenticator_enabled', $user->ID ) ) == 'enabled' ) {
167
168 // Get the users secret
169 $GA_secret = trim( get_user_option( 'googleauthenticator_secret', $user->ID ) );
170
171 // Figure out if user is using relaxed mode ?
172 $GA_relaxedmode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user->ID ) );
173
174 // Get the verification code entered by the user trying to login
175 $otp = intval( trim( $_POST[ 'otp' ] ) );
176
177 // Valid code ?
178 if ( $this->verify( $GA_secret, $otp, $GA_relaxedmode ) ) {
179 return $userstate;
180 } else {
181 // No, lets see if an app password is enabled, and this is an XMLRPC / APP login ?
182 if ( trim( get_user_option( 'googleauthenticator_pwdenabled', $user->ID ) ) == 'enabled' && ( defined('XMLRPC_REQUEST') || defined('APP_REQUEST') ) ) {
183 $GA_passwords = json_decode( get_user_option( 'googleauthenticator_passwords', $user->ID ) );
184 $passwordsha1 = trim($GA_passwords->{'password'} );
185 $usersha1 = sha1( strtoupper( str_replace( ' ', '', $password ) ) );
186 if ( $passwordsha1 == $usersha1 ) {
187 return new WP_User( $user->ID );
188 } else {
189 // Wrong XMLRPC/APP password !
190 return new WP_Error( 'invalid_google_authenticator_password', __( '<strong>ERROR</strong>: The Google Authenticator password is incorrect.', 'google-authenticator' ) );
191 }
192 } else {
193 return new WP_Error( 'invalid_google_authenticator_token', __( '<strong>ERROR</strong>: The Google Authenticator code is incorrect or has expired.', 'google-authenticator' ) );
194 }
195 }
196 }
197 // Google Authenticator isn't enabled for this account,
198 // just resume normal authentication.
199 return $userstate;
200 }
201
202
203 /**
204 * Extend personal profile page with Google Authenticator settings.
205 */
206 function profile_personal_options() {
207 global $user_id, $is_profile_page;
208
209 // If editing of Google Authenticator settings has been disabled, just return
210 $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) );
211 if ( $GA_hidefromuser == 'enabled') return;
212
213 $GA_secret = trim( get_user_option( 'googleauthenticator_secret', $user_id ) );
214 $GA_enabled = trim( get_user_option( 'googleauthenticator_enabled', $user_id ) );
215 $GA_relaxedmode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user_id ) );
216 $GA_description = trim( get_user_option( 'googleauthenticator_description', $user_id ) );
217 $GA_pwdenabled = trim( get_user_option( 'googleauthenticator_pwdenabled', $user_id ) );
218 $GA_password = trim( get_user_option( 'googleauthenticator_passwords', $user_id ) );
219
220 // We dont store the generated app password in cleartext so there is no point in trying
221 // to show the user anything except from the fact that a password exists.
222 if ( $GA_password != '' ) {
223 $GA_password = "XXXX XXXX XXXX XXXX";
224 }
225
226 // In case the user has no secret ready (new install), we create one.
227 if ( '' == $GA_secret ) {
228 $GA_secret = $this->create_secret();
229 }
230
231 // Use "WordPress Blog" as default description
232 if ( '' == $GA_description ) {
233 $GA_description = __( 'WordPress Blog', 'google-authenticator' );
234 }
235
236 echo "<h3>".__( 'Google Authenticator Settings', 'google-authenticator' )."</h3>\n";
237
238 echo "<table class=\"form-table\">\n";
239 echo "<tbody>\n";
240 echo "<tr>\n";
241 echo "<th scope=\"row\">".__( 'Active', 'google-authenticator' )."</th>\n";
242 echo "<td>\n";
243 echo "<input name=\"GA_enabled\" id=\"GA_enabled\" class=\"tog\" type=\"checkbox\"" . checked( $GA_enabled, 'enabled', false ) . "/>\n";
244 echo "</td>\n";
245 echo "</tr>\n";
246
247 // Create URL for the Google charts QR code generator.
248 $chl = urlencode( "otpauth://totp/{$GA_description}?secret={$GA_secret}" );
249 $qrcodeurl = "https://chart.googleapis.com/chart?cht=qr&amp;chs=300x300&amp;chld=H|0&amp;chl={$chl}";
250
251 if ( $is_profile_page || IS_PROFILE_PAGE ) {
252 echo "<tr>\n";
253 echo "<th scope=\"row\">".__( 'Relaxed mode', 'google-authenticator' )."</th>\n";
254 echo "<td>\n";
255 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";
256 echo "</td>\n";
257 echo "</tr>\n";
258
259 echo "<tr>\n";
260 echo "<th><label for=\"GA_description\">".__('Description','google-authenticator')."</label></th>\n";
261 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";
262 echo "</tr>\n";
263
264 echo "<tr>\n";
265 echo "<th><label for=\"GA_secret\">".__('Secret','google-authenticator')."</label></th>\n";
266 echo "<td>\n";
267 echo "<input name=\"GA_secret\" id=\"GA_secret\" value=\"{$GA_secret}\" readonly=\"readonly\" type=\"text\" size=\"25\" />";
268 echo "<input name=\"GA_newsecret\" id=\"GA_newsecret\" value=\"".__("Create new secret",'google-authenticator')."\" type=\"button\" class=\"button\" />";
269 echo "<input name=\"show_qr\" id=\"show_qr\" value=\"".__("Show/Hide QR code",'google-authenticator')."\" type=\"button\" class=\"button\" onclick=\"jQuery('#GA_QR_INFO').toggle('slow');\" />";
270 echo "</td>\n";
271 echo "</tr>\n";
272
273 echo "<tr>\n";
274 echo "<th></th>\n";
275 echo "<td><div id=\"GA_QR_INFO\" style=\"display: none\" >";
276 echo "<img id=\"GA_QRCODE\" src=\"{$qrcodeurl}\" alt=\"QR Code\"/>";
277 echo '<span class="description"><br/> ' . __( 'Scan this with the Google Authenticator app.', 'google-authenticator' ) . '</span>';
278 echo "</div></td>\n";
279 echo "</tr>\n";
280
281 echo "<tr>\n";
282 echo "<th scope=\"row\">".__( 'Enable App password', 'google-authenticator' )."</th>\n";
283 echo "<td>\n";
284 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";
285 echo "</td>\n";
286 echo "</tr>\n";
287
288 echo "<tr>\n";
289 echo "<th></th>\n";
290 echo "<td>\n";
291 echo "<input name=\"GA_password\" id=\"GA_password\" readonly=\"readonly\" value=\"".$GA_password."\" type=\"text\" size=\"25\" />";
292 echo "<input name=\"GA_createpassword\" id=\"GA_createpassword\" value=\"".__("Create new password",'google-authenticator')."\" type=\"button\" class=\"button\" />";
293 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";
294 echo "</td>\n";
295 echo "</tr>\n";
296 }
297
298
299 echo "</tbody></table>\n";
300 echo "<script type=\"text/javascript\">\n";
301 echo "var GAnonce='".wp_create_nonce('GoogleAuthenticatoraction')."';\n";
302 echo <<<ENDOFJS
303 var pwdata;
304 jQuery('#GA_newsecret').bind('click', function() {
305 var data=new Object();
306 data['action'] = 'GoogleAuthenticator_action';
307 data['nonce'] = GAnonce;
308 jQuery.post(ajaxurl, data, function(response) {
309 jQuery('#GA_secret').val(response['new-secret']);
310 chl=escape("otpauth://totp/"+jQuery('#GA_description').val()+"?secret="+jQuery('#GA_secret').val());
311 qrcodeurl="https://chart.googleapis.com/chart?cht=qr&chs=300x300&chld=H|0&chl="+chl;
312 jQuery('#GA_QRCODE').attr('src',qrcodeurl);
313 jQuery('#GA_QR_INFO').show('slow');
314 });
315 });
316
317 jQuery('#GA_description').bind('focus blur change keyup', function() {
318 chl=escape("otpauth://totp/"+jQuery('#GA_description').val()+"?secret="+jQuery('#GA_secret').val());
319 qrcodeurl="https://chart.googleapis.com/chart?cht=qr&chs=300x300&chld=H|0&chl="+chl;
320 jQuery('#GA_QRCODE').attr('src',qrcodeurl);
321 });
322
323 jQuery('#GA_createpassword').bind('click',function() {
324 var data=new Object();
325 data['action'] = 'GoogleAuthenticator_action';
326 data['nonce'] = GAnonce;
327 data['save'] = 1;
328 jQuery.post(ajaxurl, data, function(response) {
329 jQuery('#GA_password').val(response['new-secret'].match(new RegExp(".{0,4}","g")).join(' '));
330 jQuery('#GA_passworddesc').show();
331 });
332 });
333
334 jQuery('#GA_enabled').bind('change',function() {
335 GoogleAuthenticator_apppasswordcontrol();
336 });
337
338 jQuery(document).ready(function() {
339 jQuery('#GA_passworddesc').hide();
340 GoogleAuthenticator_apppasswordcontrol();
341 });
342
343 function GoogleAuthenticator_apppasswordcontrol() {
344 if (jQuery('#GA_enabled').is(':checked')) {
345 jQuery('#GA_pwdenabled').removeAttr('disabled');
346 jQuery('#GA_createpassword').removeAttr('disabled');
347 } else {
348 jQuery('#GA_pwdenabled').removeAttr('checked')
349 jQuery('#GA_pwdenabled').attr('disabled', true);
350 jQuery('#GA_createpassword').attr('disabled', true);
351 }
352 }
353 </script>
354 ENDOFJS;
355
356 }
357
358 /**
359 * Form handling of Google Authenticator options added to personal profile page (user editing his own profile)
360 */
361 function personal_options_update() {
362 global $user_id;
363
364 // If editing of Google Authenticator settings has been disabled, just return
365 $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) );
366 if ( $GA_hidefromuser == 'enabled') return;
367
368
369 $GA_enabled = ! empty( $_POST['GA_enabled'] );
370 $GA_description = trim( $_POST['GA_description'] );
371 $GA_relaxedmode = ! empty( $_POST['GA_relaxedmode'] );
372 $GA_secret = trim( $_POST['GA_secret'] );
373 $GA_pwdenabled = ! empty( $_POST['GA_pwdenabled'] );
374 $GA_password = str_replace(' ', '', trim( $_POST['GA_password'] ) );
375
376 if ( ! $GA_enabled ) {
377 $GA_enabled = 'disabled';
378 } else {
379 $GA_enabled = 'enabled';
380 }
381
382 if ( ! $GA_relaxedmode ) {
383 $GA_relaxedmode = 'disabled';
384 } else {
385 $GA_relaxedmode = 'enabled';
386 }
387
388
389 if ( ! $GA_pwdenabled ) {
390 $GA_pwdenabled = 'disabled';
391 } else {
392 $GA_pwdenabled = 'enabled';
393 }
394
395 // Only store password if a new one has been generated.
396 if (strtoupper($GA_password) != 'XXXXXXXXXXXXXXXX' ) {
397 // Store the password in a format that can be expanded easily later on if needed.
398 $GA_password = array( 'appname' => 'Default', 'password' => sha1( $GA_password ) );
399 update_user_option( $user_id, 'googleauthenticator_passwords', json_encode( $GA_password ), true );
400 }
401
402 update_user_option( $user_id, 'googleauthenticator_enabled', $GA_enabled, true );
403 update_user_option( $user_id, 'googleauthenticator_description', $GA_description, true );
404 update_user_option( $user_id, 'googleauthenticator_relaxedmode', $GA_relaxedmode, true );
405 update_user_option( $user_id, 'googleauthenticator_secret', $GA_secret, true );
406 update_user_option( $user_id, 'googleauthenticator_pwdenabled', $GA_pwdenabled, true );
407
408 }
409
410 /**
411 * Extend profile page with ability to enable/disable Google Authenticator authentication requirement.
412 * Used by an administrator when editing other users.
413 */
414 function edit_user_profile() {
415 global $user_id;
416 $GA_enabled = trim( get_user_option( 'googleauthenticator_enabled', $user_id ) );
417 $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) );
418 echo "<h3>".__('Google Authenticator Settings','google-authenticator')."</h3>\n";
419 echo "<table class=\"form-table\">\n";
420 echo "<tbody>\n";
421
422 echo "<tr>\n";
423 echo "<th scope=\"row\">".__('Hide settings from user','google-authenticator')."</th>\n";
424 echo "<td>\n";
425 echo "<div><input name=\"GA_hidefromuser\" id=\"GA_hidefromuser\" class=\"tog\" type=\"checkbox\"" . checked( $GA_hidefromuser, 'enabled', false ) . "/>\n";
426 echo "</td>\n";
427 echo "</tr>\n";
428
429 echo "<tr>\n";
430 echo "<th scope=\"row\">".__('Active','google-authenticator')."</th>\n";
431 echo "<td>\n";
432 echo "<div><input name=\"GA_enabled\" id=\"GA_enabled\" class=\"tog\" type=\"checkbox\"" . checked( $GA_enabled, 'enabled', false ) . "/>\n";
433 echo "</td>\n";
434 echo "</tr>\n";
435
436 echo "</tbody>\n";
437 echo "</table>\n";
438 }
439
440 /**
441 * Form handling of Google Authenticator options on edit profile page (admin user editing other user)
442 */
443 function edit_user_profile_update() {
444 global $user_id;
445
446 $GA_enabled = ! empty( $_POST['GA_enabled'] );
447 $GA_hidefromuser = ! empty( $_POST['GA_hidefromuser'] );
448
449 if ( ! $GA_enabled ) {
450 $GA_enabled = 'disabled';
451 } else {
452 $GA_enabled = 'enabled';
453 }
454
455 if ( ! $GA_hidefromuser ) {
456 $GA_hidefromuser = 'disabled';
457 } else {
458 $GA_hidefromuser = 'enabled';
459 }
460
461 update_user_option( $user_id, 'googleauthenticator_enabled', $GA_enabled, true );
462 update_user_option( $user_id, 'googleauthenticator_hidefromuser', $GA_hidefromuser, true );
463
464 }
465
466
467 /**
468 * AJAX callback function used to generate new secret
469 */
470 function ajax_callback() {
471 global $user_id;
472
473 // Some AJAX security
474 check_ajax_referer( 'GoogleAuthenticatoraction', 'nonce' );
475
476 // Create new secret, using the users password hash as input for further hashing
477 $secret = $this->create_secret();
478
479 $result = array( 'new-secret' => $secret );
480 header( 'Content-Type: application/json' );
481 echo json_encode( $result );
482
483 // die() is required to return a proper result
484 die();
485 }
486
487 } // end class
488
489 new GoogleAuthenticator;
490 ?>
491