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

463 lines 17.4 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.42
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 $GA_secret = trim( get_user_option( 'googleauthenticator_secret', $user_id ) );
210 $GA_enabled = trim( get_user_option( 'googleauthenticator_enabled', $user_id ) );
211 $GA_relaxedmode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user_id ) );
212 $GA_description = trim( get_user_option( 'googleauthenticator_description', $user_id ) );
213 $GA_pwdenabled = trim( get_user_option( 'googleauthenticator_pwdenabled', $user_id ) );
214 $GA_password = trim( get_user_option( 'googleauthenticator_passwords', $user_id ) );
215
216 // We dont store the generated app password in cleartext so there is no point in trying
217 // to show the user anything except from the fact that a password exists.
218 if ( $GA_password != '' ) {
219 $GA_password = "XXXX XXXX XXXX XXXX";
220 }
221
222 // In case the user has no secret ready (new install), we create one.
223 if ( '' == $GA_secret ) {
224 $GA_secret = $this->create_secret();
225 }
226
227 // Use "WordPress Blog" as default description
228 if ( '' == $GA_description ) {
229 $GA_description = __( 'WordPress Blog', 'google-authenticator' );
230 }
231
232 echo "<h3>".__( 'Google Authenticator Settings', 'google-authenticator' )."</h3>\n";
233
234 echo "<table class=\"form-table\">\n";
235 echo "<tbody>\n";
236 echo "<tr>\n";
237 echo "<th scope=\"row\">".__( 'Active', 'google-authenticator' )."</th>\n";
238 echo "<td>\n";
239 echo "<input name=\"GA_enabled\" id=\"GA_enabled\" class=\"tog\" type=\"checkbox\"" . checked( $GA_enabled, 'enabled', false ) . "/>\n";
240 echo "</td>\n";
241 echo "</tr>\n";
242
243 // Create URL for the Google charts QR code generator.
244 $chl = urlencode( "otpauth://totp/{$GA_description}?secret={$GA_secret}" );
245 $qrcodeurl = "https://chart.googleapis.com/chart?cht=qr&amp;chs=300x300&amp;chld=H|0&amp;chl={$chl}";
246
247 if ( $is_profile_page || IS_PROFILE_PAGE ) {
248 echo "<tr>\n";
249 echo "<th scope=\"row\">".__( 'Relaxed mode', 'google-authenticator' )."</th>\n";
250 echo "<td>\n";
251 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";
252 echo "</td>\n";
253 echo "</tr>\n";
254
255 echo "<tr>\n";
256 echo "<th><label for=\"GA_description\">".__('Description','google-authenticator')."</label></th>\n";
257 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";
258 echo "</tr>\n";
259
260 echo "<tr>\n";
261 echo "<th><label for=\"GA_secret\">".__('Secret','google-authenticator')."</label></th>\n";
262 echo "<td>\n";
263 echo "<input name=\"GA_secret\" id=\"GA_secret\" value=\"{$GA_secret}\" readonly=\"readonly\" type=\"text\" size=\"25\" />";
264 echo "<input name=\"GA_newsecret\" id=\"GA_newsecret\" value=\"".__("Create new secret",'google-authenticator')."\" type=\"button\" class=\"button\" />";
265 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');\" />";
266 echo "</td>\n";
267 echo "</tr>\n";
268
269 echo "<tr>\n";
270 echo "<th></th>\n";
271 echo "<td><div id=\"GA_QR_INFO\" style=\"display: none\" >";
272 echo "<img id=\"GA_QRCODE\" src=\"{$qrcodeurl}\" alt=\"QR Code\"/>";
273 echo '<span class="description"><br/> ' . __( 'Scan this with the Google Authenticator app.', 'google-authenticator' ) . '</span>';
274 echo "</div></td>\n";
275 echo "</tr>\n";
276
277 echo "<tr>\n";
278 echo "<th scope=\"row\">".__( 'Enable App password', 'google-authenticator' )."</th>\n";
279 echo "<td>\n";
280 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";
281 echo "</td>\n";
282 echo "</tr>\n";
283
284 echo "<tr>\n";
285 echo "<th></th>\n";
286 echo "<td>\n";
287 echo "<input name=\"GA_password\" id=\"GA_password\" readonly=\"readonly\" value=\"".$GA_password."\" type=\"text\" size=\"25\" />";
288 echo "<input name=\"GA_createpassword\" id=\"GA_createpassword\" value=\"".__("Create new password",'google-authenticator')."\" type=\"button\" class=\"button\" />";
289 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";
290 echo "</td>\n";
291 echo "</tr>\n";
292 }
293
294
295 echo "</tbody></table>\n";
296 echo "<script type=\"text/javascript\">\n";
297 echo "var GAnonce='".wp_create_nonce('GoogleAuthenticatoraction')."';\n";
298 echo <<<ENDOFJS
299 var pwdata;
300 jQuery('#GA_newsecret').bind('click', function() {
301 var data=new Object();
302 data['action'] = 'GoogleAuthenticator_action';
303 data['nonce'] = GAnonce;
304 jQuery.post(ajaxurl, data, function(response) {
305 jQuery('#GA_secret').val(response['new-secret']);
306 chl=escape("otpauth://totp/"+jQuery('#GA_description').val()+"?secret="+jQuery('#GA_secret').val());
307 qrcodeurl="https://chart.googleapis.com/chart?cht=qr&chs=300x300&chld=H|0&chl="+chl;
308 jQuery('#GA_QRCODE').attr('src',qrcodeurl);
309 jQuery('#GA_QR_INFO').show('slow');
310 });
311 });
312
313 jQuery('#GA_description').bind('focus blur change keyup', function() {
314 chl=escape("otpauth://totp/"+jQuery('#GA_description').val()+"?secret="+jQuery('#GA_secret').val());
315 qrcodeurl="https://chart.googleapis.com/chart?cht=qr&chs=300x300&chld=H|0&chl="+chl;
316 jQuery('#GA_QRCODE').attr('src',qrcodeurl);
317 });
318
319 jQuery('#GA_createpassword').bind('click',function() {
320 var data=new Object();
321 data['action'] = 'GoogleAuthenticator_action';
322 data['nonce'] = GAnonce;
323 data['save'] = 1;
324 jQuery.post(ajaxurl, data, function(response) {
325 jQuery('#GA_password').val(response['new-secret'].match(new RegExp(".{0,4}","g")).join(' '));
326 jQuery('#GA_passworddesc').show();
327 });
328 });
329
330 jQuery('#GA_enabled').bind('change',function() {
331 GoogleAuthenticator_apppasswordcontrol();
332 });
333
334 jQuery(document).ready(function() {
335 jQuery('#GA_passworddesc').hide();
336 GoogleAuthenticator_apppasswordcontrol();
337 });
338
339 function GoogleAuthenticator_apppasswordcontrol() {
340 if (jQuery('#GA_enabled').is(':checked')) {
341 jQuery('#GA_pwdenabled').removeAttr('disabled');
342 jQuery('#GA_createpassword').removeAttr('disabled');
343 } else {
344 jQuery('#GA_pwdenabled').removeAttr('checked')
345 jQuery('#GA_pwdenabled').attr('disabled', true);
346 jQuery('#GA_createpassword').attr('disabled', true);
347 }
348 }
349 </script>
350 ENDOFJS;
351
352 }
353
354 /**
355 * Form handling of Google Authenticator options added to personal profile page (user editing his own profile)
356 */
357 function personal_options_update() {
358 global $user_id;
359
360 $GA_enabled = ! empty( $_POST['GA_enabled'] );
361 $GA_description = trim( $_POST['GA_description'] );
362 $GA_relaxedmode = ! empty( $_POST['GA_relaxedmode'] );
363 $GA_secret = trim( $_POST['GA_secret'] );
364 $GA_pwdenabled = ! empty( $_POST['GA_pwdenabled'] );
365 $GA_password = str_replace(' ', '', trim( $_POST['GA_password'] ) );
366
367 if ( ! $GA_enabled ) {
368 $GA_enabled = 'disabled';
369 } else {
370 $GA_enabled = 'enabled';
371 }
372
373 if ( ! $GA_relaxedmode ) {
374 $GA_relaxedmode = 'disabled';
375 } else {
376 $GA_relaxedmode = 'enabled';
377 }
378
379
380 if ( ! $GA_pwdenabled ) {
381 $GA_pwdenabled = 'disabled';
382 } else {
383 $GA_pwdenabled = 'enabled';
384 }
385
386 // Only store password if a new one has been generated.
387 if (strtoupper($GA_password) != 'XXXXXXXXXXXXXXXX' ) {
388 // Store the password in a format that can be expanded easily later on if needed.
389 $GA_password = array( 'appname' => 'Default', 'password' => sha1( $GA_password ) );
390 update_user_option( $user_id, 'googleauthenticator_passwords', json_encode( $GA_password ), true );
391 }
392
393 update_user_option( $user_id, 'googleauthenticator_enabled', $GA_enabled, true );
394 update_user_option( $user_id, 'googleauthenticator_description', $GA_description, true );
395 update_user_option( $user_id, 'googleauthenticator_relaxedmode', $GA_relaxedmode, true );
396 update_user_option( $user_id, 'googleauthenticator_secret', $GA_secret, true );
397 update_user_option( $user_id, 'googleauthenticator_pwdenabled', $GA_pwdenabled, true );
398
399 }
400
401 /**
402 * Extend profile page with ability to enable/disable Google Authenticator authentication requirement.
403 * Used by an administrator when editing other users.
404 */
405 function edit_user_profile() {
406 global $user_id;
407 $GA_enabled = trim( get_user_option( 'googleauthenticator_enabled', $user_id ) );
408 echo "<h3>".__('Google Authenticator Settings','google-authenticator')."</h3>\n";
409 echo "<table class=\"form-table\">\n";
410 echo "<tbody>\n";
411 echo "<tr>\n";
412 echo "<th scope=\"row\">".__('Active','google-authenticator')."</th>\n";
413 echo "<td>\n";
414 echo "<div><input name=\"GA_enabled\" id=\"GA_enabled\" class=\"tog\" type=\"checkbox\"" . checked( $GA_enabled, 'enabled', false ) . "/>\n";
415 echo "</td>\n";
416 echo "</tr>\n";
417 echo "</tbody>\n";
418 echo "</table>\n";
419 }
420
421 /**
422 * Form handling of Google Authenticator options on edit profile page (admin user editing other user)
423 */
424 function edit_user_profile_update() {
425 global $user_id;
426
427 $GA_enabled = ! empty( $_POST['GA_enabled'] );
428
429 if ( ! $GA_enabled ) {
430 $GA_enabled = 'disabled';
431 } else {
432 $GA_enabled = 'enabled';
433 }
434
435 update_user_option( $user_id, 'googleauthenticator_enabled', $GA_enabled, true );
436 }
437
438
439 /**
440 * AJAX callback function used to generate new secret
441 */
442 function ajax_callback() {
443 global $user_id;
444
445 // Some AJAX security
446 check_ajax_referer( 'GoogleAuthenticatoraction', 'nonce' );
447
448 // Create new secret, using the users password hash as input for further hashing
449 $secret = $this->create_secret();
450
451 $result = array( 'new-secret' => $secret );
452 header( 'Content-Type: application/json' );
453 echo json_encode( $result );
454
455 // die() is required to return a proper result
456 die();
457 }
458
459 } // end class
460
461 new GoogleAuthenticator;
462 ?>
463