diff -r d109af008343 -r 6212d849ab08 yubikey/auth.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/yubikey/auth.php Fri Jun 30 17:49:12 2017 -0400 @@ -0,0 +1,325 @@ +attachHook('login_process_userdata_json', 'return yubikey_auth_hook_json($userinfo, $req["level"], @$req["remember"]);'); +// hook into special page init +$plugins->attachHook('session_started', 'yubikey_add_special_pages();'); +// session key security +$plugins->attachHook('session_key_calc', 'yubikey_sk_calc($user_id, $key_pieces, $sk_mode);'); + +function yubikey_auth_hook_json(&$userdata, $level, $remember) +{ + global $db, $session, $paths, $template, $plugins; // Common objects + global $lang; + + $do_validate_otp = false; + $do_validate_user = false; + $do_validate_pass = false; + + $user_flag = ( $level >= USER_LEVEL_CHPREF ) ? YK_SEC_ELEV_USERNAME : YK_SEC_NORMAL_USERNAME; + $pass_flag = ( $level >= USER_LEVEL_CHPREF ) ? YK_SEC_ELEV_PASSWORD : YK_SEC_NORMAL_PASSWORD; + + $auth_log_prefix = ( $level >= USER_LEVEL_CHPREF ) ? 'admin_' : ''; + + // Sort of a hack: if the password looks like an OTP and the OTP field is empty, use the password as the OTP + if ( empty($userdata['yubikey_otp']) && preg_match('/^[cbdefghijklnrtuv]{44}$/', $userdata['password'] ) ) + { + $userdata['yubikey_otp'] = $userdata['password']; + } + + // Lockouts removed from here - they're done during preprocessing now. + + if ( !empty($userdata['username']) ) + { + // get flags + $q = $db->sql_query('SELECT user_id, user_yubikey_flags FROM ' . table_prefix . "users WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '" . $db->escape(strtolower($userdata['username'])) . "';"); + if ( !$q ) + $db->die_json(); + + if ( $db->numrows() < 1 ) + { + // Username not found - let the main login function handle it + $db->free_result(); + return null; + } + list($user_id, $flags) = $db->fetchrow_num(); + $flags = intval($flags); + // At the point the username is validated. + $do_validate_user = false; + $do_validate_pass = $flags & $pass_flag; + if ( empty($userdata['yubikey_otp']) ) + { + // no OTP was provided + // make sure the user has allowed logging in with no OTP + if ( !($flags & YK_SEC_ALLOW_NO_OTP) ) + { + // We also might have no Yubikeys enrolled. + $q = $db->sql_query('SELECT 1 FROM ' . table_prefix . "yubikey WHERE user_id = $user_id;"); + if ( !$q ) + $db->die_json(); + + if ( $db->numrows() > 0 ) + { + // Yep at least one key is enrolled. + // I don't think these should be logged because they'll usually just be innocent mistakes. + $db->free_result(); + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_must_have_otp' + ); + } + // Nope, no keys enrolled, user hasn't enabled Yubikey support + $db->free_result(); + } + // we're ok, use normal password auth + return null; + } + else + { + // user did enter an OTP; make sure it's associated with the username + $yubi_uid = $db->escape(substr($userdata['yubikey_otp'], 0, 12)); + $q = $db->sql_query('SELECT user_id FROM ' . table_prefix . 'yubikey WHERE yubi_uid = \'' . $yubi_uid . '\';'); + if ( !$q ) + $db->die_json(); + if ( $db->numrows() < 1 ) + { + $db->free_result(); + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_key_not_authorized' + ); + } + list($yubi_pair_uid) = $db->fetchrow_num(); + if ( $yubi_pair_uid !== $user_id ) + { + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_uid_mismatch' + ); + } + $db->free_result(); + $do_validate_otp = true; + } + } + else if ( !empty($userdata['yubikey_otp']) ) + { + // we have an OTP, but no username to work with + $yubi_uid = substr($userdata['yubikey_otp'], 0, 12); + if ( !preg_match('/^[cbdefghijklnrtuv]{12}$/', $yubi_uid ) ) + { + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_invalid_otp' + ); + } + $q = $db->sql_query('SELECT u.user_id, u.username, u.user_yubikey_flags FROM ' . table_prefix . "users AS u\n" + . " LEFT JOIN " . table_prefix . "yubikey AS y\n" + . " ON ( y.user_id = u.user_id )\n" + . " WHERE y.yubi_uid = '$yubi_uid'\n" + . " GROUP BY u.user_yubikey_flags;"); + if ( !$q ) + $db->_die(); + + if ( $db->numrows() < 1 ) + { + if ( !$do_validate_pass ) + $session->sql('INSERT INTO ' . table_prefix . "logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES\n" + . ' (\'security\', \'' . $auth_log_prefix . 'auth_bad\', '.time().', \'DEPRECATED\', \'(Yubikey)\', ' + . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); + + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_key_not_authorized' + ); + } + + list($user_id, $username, $flags) = $db->fetchrow_num(); + + if ( $level > USER_LEVEL_MEMBER ) + { + $session->start(); + if ( $session->user_logged_in && ($session->user_id !== $user_id) ) + { + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_uid_mismatch' + ); + } + } + + $do_validate_otp = true; + $do_validate_user = $flags & $user_flag; + $do_validate_pass = $flags & $pass_flag; + // to complete security logs later + $userdata['username'] = $username; + } + else + { + // Nothing - no username or OTP. This request can't be used; throw it out. + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_nothing_provided' + ); + } + + if ( $do_validate_otp ) + { + // We need to validate the OTP. + $otp_check = yubikey_validate_otp($userdata['yubikey_otp']); + if ( !$otp_check['success'] ) + { + if ( !$do_validate_pass ) + $session->sql('INSERT INTO ' . table_prefix . "logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES\n" + . ' (\'security\', \'' . $auth_log_prefix . 'auth_bad\', '.time().', \'DEPRECATED\', \'(Yubikey)\', ' + . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); + + if ( $otp_check['error'] === 'http_failed' ) + { + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_' . $otp_check['error'], + 'http_error' => $otp_check['http_error'] + ); + } + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_' . $otp_check['error'] + ); + } + } + if ( $do_validate_user ) + { + if ( empty($username) ) + { + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_must_have_username' + ); + } + if ( strtolower($username) !== strtolower($userdata['username']) ) + { + // Username incorrect + if ( !$do_validate_pass ) + $session->sql('INSERT INTO ' . table_prefix . "logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES\n" + . ' (\'security\', \'' . $auth_log_prefix . 'auth_bad\', '.time().', \'DEPRECATED\', \'(Yubikey)\', ' + . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); + return array( + 'mode' => 'error', + 'error' => 'invalid_credentials' + ); + } + } + // Do we need to have the password validated? + if ( $do_validate_pass ) + { + if ( empty($userdata['password']) ) + { + return array( + 'mode' => 'error', + 'error' => 'yubiauth_err_must_have_password' + ); + } + // Yes; return and let the login API continue + return null; + } + else + { + // No password required; validated, issue session key + $session->sql('INSERT INTO ' . table_prefix . "logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES\n" + . ' (\'security\', \'' . $auth_log_prefix . 'auth_good\', '.time().', \'DEPRECATED\', \'' . $db->escape($userdata['username']) . '\', ' + . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); + + $q = $db->sql_query('SELECT password FROM ' . table_prefix . "users WHERE user_id = $user_id;"); + if ( !$q ) + $db->_die(); + + list($password) = $db->fetchrow_num(); + $db->free_result(); + + $session->register_session($user_id, $userdata['username'], $password, intval($level), $remember); + return true; + } +} + +function yubikey_add_special_pages() +{ + global $db, $session, $paths, $template, $plugins; // Common objects + global $lang; + + if ( getConfig('yubikey_enable', '1') != '1' ) + return true; + + $paths->add_page(array( + 'name' => $lang->get('yubiauth_specialpage_yubikey'), + 'urlname' => 'Yubikey', + 'namespace' => 'Special', + 'visible' => 0, 'protected' => 0, 'comments_on' => 0, 'special' => 0 + )); +} + +function yubikey_sk_calc($user_id, &$key_pieces, &$sk_mode) +{ + global $db, $session, $paths, $template, $plugins; // Common objects + // hash the user's yubikeys + $q = $db->sql_query('SELECT yubi_uid FROM ' . table_prefix . "yubikey WHERE user_id = $user_id;"); + if ( !$q ) + $db->_die(); + + while ( $row = $db->fetchrow() ) + { + $key_pieces[] = $row['yubi_uid']; + } +} + +function page_Special_Yubikey() +{ + global $db, $session, $paths, $template, $plugins; // Common objects + + header('Content-type: text/javascript'); + /* + if ( isset($_GET['validate_otp']) ) + { + echo enano_json_encode(yubikey_validate_otp($_GET['validate_otp'])); + return true; + } + */ + if ( isset($_GET['get_flags']) || isset($_POST['get_flags']) ) + { + $yubi_uid = substr($_REQUEST['get_flags'], 0, 12); + if ( !preg_match('/^[cbdefghijklnrtuv]{12}$/', $yubi_uid) ) + { + return print enano_json_encode(array( + 'mode' => 'error', + 'error' => 'invalid_otp' + )); + } + $q = $db->sql_query('SELECT u.user_yubikey_flags FROM ' . table_prefix . "users AS u\n" + . " LEFT JOIN " . table_prefix . "yubikey AS y\n" + . " ON ( y.user_id = u.user_id )\n" + . " WHERE y.yubi_uid = '$yubi_uid'\n" + . " GROUP BY u.user_yubikey_flags;"); + if ( !$q ) + $db->_die(); + + if ( $db->numrows() < 1 ) + { + return print enano_json_encode(array( + 'mode' => 'error', + 'error' => 'key_not_authorized' + )); + } + + list($flags) = $db->fetchrow_num(); + + echo enano_json_encode(array( + // We strip YK_SEC_ALLOW_NO_OTP here for security reasons. + 'flags' => intval($flags & ~YK_SEC_ALLOW_NO_OTP) + )); + + return true; + } +} +