is_two_screen_signin_enabled() ) { add_action( 'login_form', array( $this, 'loginform' ) ); add_action( 'login_footer', array( $this, 'loginfooter' ) ); } add_filter( 'authenticate', array( $this, 'check_otp' ), 50, 3 ); if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { add_action( 'wp_ajax_GoogleAuthenticator_action', array( $this, 'ajax_callback' ) ); } add_action( 'personal_options_update', array( $this, 'personal_options_update' ) ); add_action( 'profile_personal_options', array( $this, 'profile_personal_options' ) ); add_action( 'edit_user_profile', array( $this, 'edit_user_profile' ) ); add_action( 'edit_user_profile_update', array( $this, 'edit_user_profile_update' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'add_qrcode_script' ) ); add_action( 'admin_menu', array ( $this, 'add_pages' ) ); add_action( 'network_admin_menu', array ( $this, 'add_pages' ) ); add_action( 'current_screen', array ( $this, 'redirect_if_setup_required' ) ); add_action( 'admin_notices', array ( $this, 'successful_signup_message' ) ); add_action( 'load-admin_page_google_authenticator_user_page', array( $this, 'save_submitted_setup_page' ) ); load_plugin_textdomain( 'google-authenticator', false, basename( dirname( __FILE__ ) ) . '/lang' ); } /** * Whether we show Google Auth code on the login screen, or after the user has entered their username and password. * * If it's on a separate screen, it means username / passwords can still be bruteforced, but logins can't occur without 2fa * * @return bool */ function is_two_screen_signin_enabled() { $two_screen_mfa = is_multisite() ? get_site_option( 'googleauthenticator_two_screen_signin' ) : get_option( 'googleauthenticator_two_screen_signin' ); return !! $two_screen_mfa; } /** * Check the verification code entered by the user. */ function verify( $secretkey, $thistry, $relaxedmode, $lasttimeslot ) { // Did the user enter 6 digits ? if ( strlen( $thistry ) != 6) { return false; } else { $thistry = intval ( $thistry ); } // If user is running in relaxed mode, we allow more time drifting // ±4 min, as opposed to ± 30 seconds in normal mode. if ( $relaxedmode == 'enabled' ) { $firstcount = -8; $lastcount = 8; } else { $firstcount = -1; $lastcount = 1; } $tm = floor( time() / 30 ); $secretkey=Base32::decode($secretkey); // Keys from 30 seconds before and after are valid aswell. for ($i=$firstcount; $i<=$lastcount; $i++) { // Pack time into binary string $time=chr(0).chr(0).chr(0).chr(0).pack('N*',$tm+$i); // Hash it with users secret key $hm = hash_hmac( 'SHA1', $time, $secretkey, true ); // Use last nipple of result as index/offset $offset = ord(substr($hm,-1)) & 0x0F; // grab 4 bytes of the result $hashpart=substr($hm,$offset,4); // Unpak binary value $value=unpack("N",$hashpart); $value=$value[1]; // Only 32 bits $value = $value & 0x7FFFFFFF; $value = $value % 1000000; if ( $value === $thistry ) { // Check for replay (Man-in-the-middle) attack. // Since this is not Star Trek, time can only move forward, // meaning current login attempt has to be in the future compared to // last successful login. if ( $lasttimeslot >= ($tm+$i) ) { error_log("Google Authenticator plugin: Man-in-the-middle attack detected (Could also be 2 legit login attempts within the same 30 second period)"); return false; } // Return timeslot in which login happened. return $tm+$i; } } return false; } /** * Create a new random secret for the Google Authenticator app. * 16 characters, randomly chosen from the allowed Base32 characters * equals 10 bytes = 80 bits, as 256^10 = 32^16 = 2^80 */ function create_secret() { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // allowed characters in Base32 $secret = ''; for ( $i = 0; $i < 16; $i++ ) { $secret .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 ); } return $secret; } /** * Add the script to generate QR codes. */ function add_qrcode_script() { wp_enqueue_script('jquery'); wp_register_script('qrcode_script', plugins_url('jquery.qrcode.min.js', __FILE__),array("jquery")); wp_enqueue_script('qrcode_script'); } /** * Add 2fa pages to menus */ function add_pages() { // No menu entry for this page add_submenu_page( null, esc_html__( 'Google Authenticator', 'google-authenticator' ), null, 'read', self::SETUP_PAGE, array( $this, 'user_setup_page' ) ); // Site admin screen 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' ) ); // Network admin screen 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' ) ); } /** * Determine if a user needs to setup authy 2fa * @return bool */ function user_needs_to_setup_google_authenticator() { $user = wp_get_current_user(); $enabled = trim(get_user_option( 'googleauthenticator_enabled', $user->ID ) ) === 'enabled'; if ( $enabled ) { return false; } $must_signup = false; $user_role = $user->roles[0]; $check_single_site_admin_options = true; if ( is_multisite() ) { $roles = get_site_option( 'googleauthenticator_mandatory_mfa_roles', array() ); if ( in_array( $user_role, $roles ) ) { $must_signup = true; } $check_single_site_admin_options = '1' !== get_site_option( 'googleauthenticator_network_only' ) ; } if ( ! $must_signup && $check_single_site_admin_options ) { $roles = get_option( 'googleauthenticator_mandatory_mfa_roles', array() ); if ( in_array( $user_role, $roles ) ) { $must_signup = true; } } return apply_filters( 'google_authenticator_needs_setup', $must_signup, $user ); } /** * Send users to the signup page if they must signup. */ function redirect_if_setup_required() { if ( $this->user_needs_to_setup_google_authenticator() ) { $screen = get_current_screen(); $pagename = 'admin_page_' . self::SETUP_PAGE; if ( is_a( $screen, 'WP_Screen') && in_array( $screen->id, array( $pagename, 'profile' ) ) ) { return; } // Some check against super admin so they can enable/disable the plugin. $location = admin_url( 'admin.php?page=' . self::SETUP_PAGE ); wp_redirect( $location); exit; } } /** * Save the GA secret if valid totp is provided * @return void */ function save_submitted_setup_page() { $this->error_message = null; // Reset a previous error message if it was set $user = wp_get_current_user(); $secret = empty( $_POST['GA_secret'] ) ? false : sanitize_text_field( $_POST['GA_secret']); $otp = empty( $_POST['GA_otp_code'] ) ? false : sanitize_text_field( $_POST['GA_otp_code']); if ( ! strlen( $secret ) || ! strlen( $otp ) ) { return; } $relaxed_mode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user->ID ) ); $relaxed_mode = 'enabled' === $relaxed_mode ? 'enabled' : 'disabled'; if ( $timeslot = $this->verify( $secret, $otp, $relaxed_mode, '' ) ) { update_user_option( $user->ID, 'googleauthenticator_lasttimeslot', $timeslot, true ); update_user_option( $user->ID, 'googleauthenticator_secret', $secret, true ); update_user_option( $user->ID, 'googleauthenticator_enabled', 'enabled', true ); $location = admin_url( 'index.php?googleauthenticator=enabled' ); wp_redirect( $location ); exit; }; $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' ) ); } /** * Show the user a success message after we redirect them following successful google authenticator setup */ function successful_signup_message() { if ( ! empty( $_GET['googleauthenticator'] ) && 'enabled' === $_GET['googleauthenticator'] ) : ?>

ID ) ) === 'enabled'; if ( $enabled ) { $location = admin_url( 'index.php' ); wp_redirect( $location ); exit; } $error = $this->error_message; $app_links = array( array( 'text' => __( 'iOS: Authy', 'google-authenticator' ), 'link' => 'https://itunes.apple.com/app/authy/id494168017', ), array( 'text' => __( 'iOS: Google Authenticator', 'google-authenticator' ), 'link' => 'https://itunes.apple.com/app/google-authenticator/id388497605', ), array( 'text' => __( 'Android: Authy', 'google-authenticator' ), 'link' => 'https://play.google.com/store/apps/details?id=com.authy.authy', ), array( 'text' => __( 'Android: Google Authenticator', 'google-authenticator' ), 'link' => 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', ), array( 'text' => __( 'Windows Phone', 'google-authenticator' ), 'link' => 'https://www.microsoft.com/store/p/authenticator/9nblggh08h54', ), array( 'text' => __( 'Chrome Browser', 'google-authenticator' ), 'link' => 'https://chrome.google.com/webstore/detail/authy-chrome-extension/fhgenkpocbhhddlgkjnfghpjanffonno', ), array( 'text' => __( 'Desktop', 'google-authenticator' ), 'link' => 'https://authy.com/download/', ), ); ?>

get_error_message() ); ?>

profile_personal_options( array( 'show_active' => false, 'show_relaxed_mode' => false, 'show_description' => false, 'show_secret_qr' => true, 'show_secret_buttons' => false, 'show_authenticator_code' => true, 'show_app_password' => false, )); ?>
save_submitted_admin_setup_page( $is_network ); ?>

$role) { $this->show_role_checkbox( $role_key, $role, $is_network ); } if ( $edit_enabled ) { wp_nonce_field( 'googleauthenticator', 'googleauthenticator' ); submit_button(); } else { esc_html_e( 'Network-wide settings in effect, only a super admin can modify them.', 'google-authenticator' ); if ( current_user_can( 'manage_network' ) ) :?>
network settings', 'google-authenticator' ), network_admin_url( 'settings.php?page=google_authenticator' ) ); } } $readonly = $readonly ? ' readonly="readonly"' : ''; ?>

common_admin_setup_page(); } /** * Network admin setup screen */ function network_admin_setup_page() { $this->common_admin_setup_page( true ); } /** * Add verification code field to login form. */ function loginform() { echo "\t

\n"; echo "\t\t\n"; echo "\t

\n"; echo "\t\n"; } /** * Disable autocomplete on Google Authenticator code input field. */ function loginfooter() { echo "\n\n"; } /** * Login form handling. * Check Google Authenticator verification code, if user has been setup to do so. * @param wordpressuser / WP_Error * @return user/loginstatus */ function check_otp( $user, $username = '', $password = '' ) { // Store result of loginprocess, so far. $userstate = $user; // Get information on user, we need this in case an app password has been enabled, // since the $user var only contain an error at this point in the login flow. if ( get_user_by( 'email', $username ) === false ) { $user = get_user_by( 'login', $username ); } else { $user = get_user_by( 'email', $username ); } // Does the user have the Google Authenticator enabled ? if ( isset( $user->ID ) && trim(get_user_option( 'googleauthenticator_enabled', $user->ID ) ) == 'enabled' ) { // Get the users secret $GA_secret = trim( get_user_option( 'googleauthenticator_secret', $user->ID ) ); // Figure out if user is using relaxed mode ? $GA_relaxedmode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user->ID ) ); // Get the verification code entered by the user trying to login if ( !empty( $_POST['googleotp'] )) { // Prevent PHP notices when using app password login $otp = trim( $_POST[ 'googleotp' ] ); } else { $otp = ''; } // When was the last successful login performed ? $lasttimeslot = trim( get_user_option( 'googleauthenticator_lasttimeslot', $user->ID ) ); // Valid code ? if ( $timeslot = $this->verify( $GA_secret, $otp, $GA_relaxedmode, $lasttimeslot ) ) { // Store the timeslot in which login was successful. update_user_option( $user->ID, 'googleauthenticator_lasttimeslot', $timeslot, true ); return $userstate; } else { // No, lets see if an app password is enabled, and this is an XMLRPC / APP login ? if ( trim( get_user_option( 'googleauthenticator_pwdenabled', $user->ID ) ) == 'enabled' && ( defined('XMLRPC_REQUEST') || defined('APP_REQUEST') ) ) { $GA_passwords = json_decode( get_user_option( 'googleauthenticator_passwords', $user->ID ) ); $passwordhash = trim($GA_passwords->{'password'} ); $usersha1 = sha1( strtoupper( str_replace( ' ', '', $password ) ) ); if ( $passwordhash == $usersha1 ) { // ToDo: Remove after some time when users have migrated to new format return new WP_User( $user->ID ); // Try the new version based on thee wp_hash_password function } elseif (wp_check_password( strtoupper( str_replace( ' ', '', $password ) ), $passwordhash)) { return new WP_User( $user->ID ); } else { // Wrong XMLRPC/APP password ! return new WP_Error( 'invalid_google_authenticator_password', __( 'ERROR: The Google Authenticator password is incorrect.', 'google-authenticator' ) ); } } else { if ( ! $this->is_two_screen_signin_enabled() ) { return new WP_Error( 'invalid_google_authenticator_token', __( 'ERROR: The Google Authenticator code is incorrect or has expired.', 'google-authenticator' ) ); } else { wp_logout(); $this->secondary_login_screen(); exit; } } } } // Google Authenticator isn't enabled for this account, // just resume normal authentication. return $userstate; } function secondary_login_screen() { $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : admin_url(); login_header( esc_html__('Secondary Login Screen', 'google-authenticator' ) ); if ( array_key_exists( 'googleotp', $_REQUEST ) ) { if ( 0 === strlen( $_REQUEST[ 'googleotp'] ) ) { $error_message = __( 'ERROR: The Google Authenticator code is missing.', 'google-authenticator' ); } else { $error_message = __( 'ERROR: The Google Authenticator code is incorrect or has expired.', 'google-authenticator' ); } echo '
' . $error_message . '
'; }?>
loginform(); ?>

true, 'show_relaxed_mode' => true, 'show_description' => true, 'show_secret_qr' => false, 'show_secret_buttons' => true, 'show_authenticator_code' => false, 'show_app_password' => true, ); $args = wp_parse_args( $args, $defaults ); $user = wp_get_current_user(); $user_id = $user->ID; // If editing of Google Authenticator settings has been disabled, just return $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) ); if ( $GA_hidefromuser == 'enabled') return; $GA_secret = trim( get_user_option( 'googleauthenticator_secret', $user_id ) ); $GA_enabled = trim( get_user_option( 'googleauthenticator_enabled', $user_id ) ); $GA_relaxedmode = trim( get_user_option( 'googleauthenticator_relaxedmode', $user_id ) ); $GA_description = trim( get_user_option( 'googleauthenticator_description', $user_id ) ); $GA_pwdenabled = trim( get_user_option( 'googleauthenticator_pwdenabled', $user_id ) ); $GA_password = trim( get_user_option( 'googleauthenticator_passwords', $user_id ) ); // We dont store the generated app password in cleartext so there is no point in trying // to show the user anything except from the fact that a password exists. if ( $GA_password != '' ) { $GA_password = "XXXX XXXX XXXX XXXX"; } // In case the user has no secret ready (new install), we create one. or use the one they just posted if ( '' == $GA_secret ) { $GA_secret = array_key_exists( 'GA_secret', $_REQUEST ) ? sanitize_text_field( $_REQUEST[ 'GA_secret' ] ) : $this->create_secret(); } if ( '' == $GA_description ) { // Super admins and users with accounts on more than one site get the network name as the helpful name, // everyone else gets the site that they're on if ( is_multisite() && ( 1 < count( get_blogs_of_user( $user_id ) || is_super_admin() ) ) ) { $GA_description = sanitize_text_field( get_blog_details( get_network()->id )->blogname ); } else { $GA_description = sanitize_text_field( get_bloginfo( 'name' ) ); } } echo "

".__( 'Google Authenticator Settings', 'google-authenticator' )."

\n"; echo "\n"; echo "\n"; if ( $args['show_active'] ) { echo "\n"; echo "\n"; echo "\n"; echo "\n"; } if ( $args['show_relaxed_mode'] ) { echo "\n"; echo "\n"; echo "\n"; echo "\n"; } $show_description_style = $args['show_description'] ? '' : 'display:none'; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; $qr_style = $args['show_secret_qr'] ? '' : 'display: none'; echo "\n"; echo "\n"; if ( $args['show_secret_qr']) : ?> \n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; } if ( $args['show_authenticator_code']) { echo "\n"; echo "\n"; echo "\n"; echo "\n"; } echo "
".__( 'Active', 'google-authenticator' )."\n"; echo "\n"; echo "
" . __( 'Relaxed mode', 'google-authenticator' ) . "\n"; echo "" . __( ' Relaxed mode allows for more time drifting on your phone clock (±4 min).', 'google-authenticator' ) . "\n"; echo "
" . __( ' Description that you\'ll see in the Google Authenticator app on your phone.', 'google-authenticator' ) . "
\n"; echo ""; if ( $args['show_secret_buttons']) { echo ""; echo ""; } echo "
"; echo "
"; echo '
' . __( 'Scan this with the Google Authenticator app.', 'google-authenticator' ) . '
'; echo "
".__( 'Enable App password', 'google-authenticator' )."\n"; echo "".__(' Enabling an App password will decrease your overall login security.','google-authenticator')."\n"; echo "
\n"; echo ""; echo ""; echo " ".__(' Password is not stored in cleartext, this is your only chance to see it.','google-authenticator')."\n"; echo "
" . __( 'After adding the site to your google authy account, add your authenticator code here.', 'google-authenticator' ) . "
\n"; if ( $args['show_authenticator_code']) { submit_button( esc_html__( 'Verify Authenticator Code', 'google-authenticator' ) ); } echo " ENDOFJS; } /** * Form handling of Google Authenticator options added to personal profile page (user editing his own profile) */ function personal_options_update() { global $user_id; // If editing of Google Authenticator settings has been disabled, just return $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) ); if ( $GA_hidefromuser == 'enabled') return; $GA_enabled = ! empty( $_POST['GA_enabled'] ); $GA_description = trim( sanitize_text_field($_POST['GA_description'] ) ); $GA_relaxedmode = ! empty( $_POST['GA_relaxedmode'] ); $GA_secret = trim( $_POST['GA_secret'] ); $GA_pwdenabled = ! empty( $_POST['GA_pwdenabled'] ); $GA_password = str_replace(' ', '', trim( $_POST['GA_password'] ) ); if ( ! $GA_enabled ) { $GA_enabled = 'disabled'; } else { $GA_enabled = 'enabled'; } if ( ! $GA_relaxedmode ) { $GA_relaxedmode = 'disabled'; } else { $GA_relaxedmode = 'enabled'; } if ( ! $GA_pwdenabled ) { $GA_pwdenabled = 'disabled'; } else { $GA_pwdenabled = 'enabled'; } // Only store password if a new one has been generated. if (strtoupper($GA_password) != 'XXXXXXXXXXXXXXXX' ) { // Store the password in a format that can be expanded easily later on if needed. $GA_password = array( 'appname' => 'Default', 'password' => wp_hash_password( $GA_password ) ); update_user_option( $user_id, 'googleauthenticator_passwords', json_encode( $GA_password ), true ); } update_user_option( $user_id, 'googleauthenticator_enabled', $GA_enabled, true ); update_user_option( $user_id, 'googleauthenticator_description', $GA_description, true ); update_user_option( $user_id, 'googleauthenticator_relaxedmode', $GA_relaxedmode, true ); update_user_option( $user_id, 'googleauthenticator_secret', $GA_secret, true ); update_user_option( $user_id, 'googleauthenticator_pwdenabled', $GA_pwdenabled, true ); } /** * Extend profile page with ability to enable/disable Google Authenticator authentication requirement. * Used by an administrator when editing other users. */ function edit_user_profile() { global $user_id; $GA_enabled = trim( get_user_option( 'googleauthenticator_enabled', $user_id ) ); $GA_hidefromuser = trim( get_user_option( 'googleauthenticator_hidefromuser', $user_id ) ); echo "

".__('Google Authenticator Settings','google-authenticator')."

\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "
".__('Hide settings from user','google-authenticator')."\n"; echo "
\n"; echo "
".__('Active','google-authenticator')."\n"; echo "
\n"; echo "
\n"; } /** * Form handling of Google Authenticator options on edit profile page (admin user editing other user) */ function edit_user_profile_update() { global $user_id; $GA_enabled = ! empty( $_POST['GA_enabled'] ); $GA_hidefromuser = ! empty( $_POST['GA_hidefromuser'] ); if ( ! $GA_enabled ) { $GA_enabled = 'disabled'; } else { $GA_enabled = 'enabled'; } if ( ! $GA_hidefromuser ) { $GA_hidefromuser = 'disabled'; } else { $GA_hidefromuser = 'enabled'; } update_user_option( $user_id, 'googleauthenticator_enabled', $GA_enabled, true ); update_user_option( $user_id, 'googleauthenticator_hidefromuser', $GA_hidefromuser, true ); } /** * AJAX callback function used to generate new secret */ function ajax_callback() { global $user_id; // Some AJAX security. check_ajax_referer( 'GoogleAuthenticatoraction', 'nonce' ); // Create new secret. $secret = $this->create_secret(); $result = array( 'new-secret' => $secret ); header( 'Content-Type: application/json' ); echo json_encode( $result ); // die() is required to return a proper result die(); } } // end class $google_authenticator = new GoogleAuthenticator;