1*fca58076SAndreas Gohr<?php 2*fca58076SAndreas Gohr// must be run within Dokuwiki 3*fca58076SAndreas Gohrif (!defined('DOKU_INC')) die(); 4*fca58076SAndreas Gohr/** 5*fca58076SAndreas Gohr * Two Factor Action Plugin 6*fca58076SAndreas Gohr * 7*fca58076SAndreas Gohr * @author Mike Wilmes mwilmes@avc.edu 8*fca58076SAndreas Gohr * Big thanks to Daniel Popp and his Google 2FA code (authgoogle2fa) as a 9*fca58076SAndreas Gohr * starting reference. 10*fca58076SAndreas Gohr * 11*fca58076SAndreas Gohr * Overview: 12*fca58076SAndreas Gohr * The plugin provides for two opportunities to perform two factor 13*fca58076SAndreas Gohr * authentication. The first is on the main login page, via a code provided by 14*fca58076SAndreas Gohr * an external authenticator. The second is at a separate prompt after the 15*fca58076SAndreas Gohr * initial login. By default, all modules will process from the second login, 16*fca58076SAndreas Gohr * but a module can subscribe to accepting a password from the main login when 17*fca58076SAndreas Gohr * it makes sense, because the user has access to the code in advance. 18*fca58076SAndreas Gohr * 19*fca58076SAndreas Gohr * If a user only has configured modules that provide for login at the main 20*fca58076SAndreas Gohr * screen, the code will only be accepted at the main login screen for 21*fca58076SAndreas Gohr * security purposes. 22*fca58076SAndreas Gohr * 23*fca58076SAndreas Gohr * Modules will be called to render their configuration forms on the profile 24*fca58076SAndreas Gohr * page and to verify a user's submitted code. If any module accepts the 25*fca58076SAndreas Gohr * submitted code, then the user is granted access. 26*fca58076SAndreas Gohr * 27*fca58076SAndreas Gohr * Each module may be used to transmit a message to the user that their 28*fca58076SAndreas Gohr * account has been logged into. One module may be used as the default 29*fca58076SAndreas Gohr * transmit option. These options are handled by the parent module. 30*fca58076SAndreas Gohr */ 31*fca58076SAndreas Gohr 32*fca58076SAndreas Gohr// Create a definition for a 2FA cookie. 33*fca58076SAndreas Gohrdefine('TWOFACTOR_COOKIE', '2FA' . DOKU_COOKIE); 34*fca58076SAndreas Gohr 35*fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin 36*fca58076SAndreas Gohr{ 37*fca58076SAndreas Gohr public $success = false; 38*fca58076SAndreas Gohr private $attribute = null; 39*fca58076SAndreas Gohr private $tokenMods = null; 40*fca58076SAndreas Gohr private $otpMods = null; 41*fca58076SAndreas Gohr private $setTime = false; 42*fca58076SAndreas Gohr 43*fca58076SAndreas Gohr public function __construct() 44*fca58076SAndreas Gohr { 45*fca58076SAndreas Gohr $this->loadConfig(); 46*fca58076SAndreas Gohr // Load the attribute helper if GA is active or not requiring use of email to send the OTP. 47*fca58076SAndreas Gohr 48*fca58076SAndreas Gohr $this->attribute = $this->loadHelper('attribute', 49*fca58076SAndreas Gohr 'TwoFactor depends on the Attribute plugin, but the Attribute plugin is not installed!'); 50*fca58076SAndreas Gohr // Now figure out what modules to load and load them. 51*fca58076SAndreas Gohr $available = Twofactor_Auth_Module::_listModules(); 52*fca58076SAndreas Gohr $allmodules = Twofactor_Auth_Module::_loadModules($available); 53*fca58076SAndreas Gohr $failed = array_diff($available, array_keys($allmodules)); 54*fca58076SAndreas Gohr if (count($failed) > 0) { 55*fca58076SAndreas Gohr msg('At least one loaded module did not have a properly named class.' . ' ' . implode(', ', $failed), -1); 56*fca58076SAndreas Gohr } 57*fca58076SAndreas Gohr $this->modules = array_filter($allmodules, function ($obj) { 58*fca58076SAndreas Gohr return $obj->getConf('enable') == 1; 59*fca58076SAndreas Gohr }); 60*fca58076SAndreas Gohr // Sanity check. 61*fca58076SAndreas Gohr $this->success = (!$requireAttribute || ($this->attribute && $this->attribute->success)) && count($this->modules) > 0; 62*fca58076SAndreas Gohr } 63*fca58076SAndreas Gohr 64*fca58076SAndreas Gohr /** 65*fca58076SAndreas Gohr * Registers the event handlers. 66*fca58076SAndreas Gohr */ 67*fca58076SAndreas Gohr public function register(Doku_Event_Handler $controller) 68*fca58076SAndreas Gohr { 69*fca58076SAndreas Gohr if (!$this->success) return; 70*fca58076SAndreas Gohr 71*fca58076SAndreas Gohr $firstlogin = false; 72*fca58076SAndreas Gohr foreach ($this->modules as $mod) { 73*fca58076SAndreas Gohr $firstlogin |= $mod->canAuthLogin(); 74*fca58076SAndreas Gohr } 75*fca58076SAndreas Gohr if ($firstlogin) { 76*fca58076SAndreas Gohr $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'twofactor_login_form'); 77*fca58076SAndreas Gohr } 78*fca58076SAndreas Gohr 79*fca58076SAndreas Gohr 80*fca58076SAndreas Gohr // Manage action flow around the twofactor authentication requirements. 81*fca58076SAndreas Gohr $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'twofactor_action_process_handler', 82*fca58076SAndreas Gohr null, -999999); 83*fca58076SAndreas Gohr // Handle the twofactor login and profile actions. 84*fca58076SAndreas Gohr $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'twofactor_handle_unknown_action'); 85*fca58076SAndreas Gohr $controller->register_hook('TPL_ACTION_GET', 'BEFORE', $this, 'twofactor_get_unknown_action'); 86*fca58076SAndreas Gohr 87*fca58076SAndreas Gohr // If the user supplies a token code at login, checks it before logging the user in. 88*fca58076SAndreas Gohr $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'twofactor_before_auth_check', null, 89*fca58076SAndreas Gohr -999999); 90*fca58076SAndreas Gohr // Atempts to process the second login if the user hasn't done so already. 91*fca58076SAndreas Gohr $controller->register_hook('AUTH_LOGIN_CHECK', 'AFTER', $this, 'twofactor_after_auth_check'); 92*fca58076SAndreas Gohr $this->log('register: Session: ' . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS); 93*fca58076SAndreas Gohr 94*fca58076SAndreas Gohr } 95*fca58076SAndreas Gohr 96*fca58076SAndreas Gohr 97*fca58076SAndreas Gohr /** 98*fca58076SAndreas Gohr * Handles the login form rendering. 99*fca58076SAndreas Gohr */ 100*fca58076SAndreas Gohr public function twofactor_login_form(&$event, $param) 101*fca58076SAndreas Gohr { 102*fca58076SAndreas Gohr $this->log('twofactor_login_form: start', self::LOGGING_DEBUG); 103*fca58076SAndreas Gohr $twofa_form = form_makeTextField('otp', '', $this->getLang('twofactor_login'), '', 'block', 104*fca58076SAndreas Gohr array('size' => '50', 'autocomplete' => 'off')); 105*fca58076SAndreas Gohr $pos = $event->data->findElementByAttribute('name', 'p'); 106*fca58076SAndreas Gohr $event->data->insertElement($pos + 1, $twofa_form); 107*fca58076SAndreas Gohr } 108*fca58076SAndreas Gohr 109*fca58076SAndreas Gohr 110*fca58076SAndreas Gohr 111*fca58076SAndreas Gohr /** 112*fca58076SAndreas Gohr * Action process redirector. If logging out, processes the logout 113*fca58076SAndreas Gohr * function. If visiting the profile, sets a flag to confirm that the 114*fca58076SAndreas Gohr * profile is being viewed in order to enable OTP attribute updates. 115*fca58076SAndreas Gohr */ 116*fca58076SAndreas Gohr public function twofactor_action_process_handler(&$event, $param) 117*fca58076SAndreas Gohr { 118*fca58076SAndreas Gohr global $USERINFO, $ID, $INFO, $INPUT; 119*fca58076SAndreas Gohr $this->log('twofactor_action_process_handler: start ' . $event->data, self::LOGGING_DEBUG); 120*fca58076SAndreas Gohr // Handle logout. 121*fca58076SAndreas Gohr if ($event->data == 'logout') { 122*fca58076SAndreas Gohr $this->_logout(); 123*fca58076SAndreas Gohr return; 124*fca58076SAndreas Gohr } 125*fca58076SAndreas Gohr // Handle main login. 126*fca58076SAndreas Gohr if ($event->data == 'login') { 127*fca58076SAndreas Gohr // To support loglog or any other module that hooks login checking for success, 128*fca58076SAndreas Gohr // Confirm that the user is logged in. If not, then redirect to twofactor_login 129*fca58076SAndreas Gohr // and fail the login. 130*fca58076SAndreas Gohr if ($USERINFO && !$this->get_clearance()) { 131*fca58076SAndreas Gohr // Hijack this event. We need to resend it after 2FA is done. 132*fca58076SAndreas Gohr $event->stopPropagation(); 133*fca58076SAndreas Gohr // Send loglog an event to show the user logged in but needs OTP code. 134*fca58076SAndreas Gohr $log = array('message' => 'logged in, ' . $this->getLang('requires_otp'), 'user' => $user); 135*fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 136*fca58076SAndreas Gohr } 137*fca58076SAndreas Gohr return; 138*fca58076SAndreas Gohr } 139*fca58076SAndreas Gohr 140*fca58076SAndreas Gohr // Check to see if we are heading to the twofactor login. 141*fca58076SAndreas Gohr if ($event->data == 'twofactor_login') { 142*fca58076SAndreas Gohr // Check if we already have clearance- just in case. 143*fca58076SAndreas Gohr if ($this->get_clearance()) { 144*fca58076SAndreas Gohr // Okay, this continues on with normal processing. 145*fca58076SAndreas Gohr return; 146*fca58076SAndreas Gohr } 147*fca58076SAndreas Gohr // We will be handling this action's permissions here. 148*fca58076SAndreas Gohr $event->preventDefault(); 149*fca58076SAndreas Gohr $event->stopPropagation(); 150*fca58076SAndreas Gohr // If not logged into the main auth plugin then send there. 151*fca58076SAndreas Gohr if (!$USERINFO) { 152*fca58076SAndreas Gohr $event->result = false; 153*fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => 'login'), true, '&')); 154*fca58076SAndreas Gohr return; 155*fca58076SAndreas Gohr } 156*fca58076SAndreas Gohr if (count($this->otpMods) == 0) { 157*fca58076SAndreas Gohr $this->log('No available otp modules.', self::LOGGING_DEBUG); 158*fca58076SAndreas Gohr // There is no way to handle this login. 159*fca58076SAndreas Gohr msg($this->getLang('mustusetoken'), -1); 160*fca58076SAndreas Gohr $event->result = false; 161*fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => 'logout'), true, '&')); 162*fca58076SAndreas Gohr return; 163*fca58076SAndreas Gohr } 164*fca58076SAndreas Gohr // Otherwise handle the action. 165*fca58076SAndreas Gohr $act = $this->_process_otp($event, $param); 166*fca58076SAndreas Gohr $event->result = true; 167*fca58076SAndreas Gohr if ($act) { 168*fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => $act), true, '&')); 169*fca58076SAndreas Gohr } 170*fca58076SAndreas Gohr return; 171*fca58076SAndreas Gohr } 172*fca58076SAndreas Gohr 173*fca58076SAndreas Gohr // Is the user logged into the wiki? 174*fca58076SAndreas Gohr if (!$USERINFO) { 175*fca58076SAndreas Gohr // If not logged in, then do nothing. 176*fca58076SAndreas Gohr return; 177*fca58076SAndreas Gohr } 178*fca58076SAndreas Gohr 179*fca58076SAndreas Gohr // See if this user has any OTP methods configured. 180*fca58076SAndreas Gohr $available = count($this->tokenMods) + count($this->otpMods) > 0; 181*fca58076SAndreas Gohr // Check if this user needs to login with 2FA. 182*fca58076SAndreas Gohr // Wiki mandatory is on if user is logged in and config is mandatory 183*fca58076SAndreas Gohr $mandatory = $this->getConf("optinout") == 'mandatory' && $INPUT->server->str('REMOTE_USER', ''); 184*fca58076SAndreas Gohr // User is NOT OPTED OUT if the optin setting is undefined and the wiki config is optout. 185*fca58076SAndreas Gohr $not_opted_out = $this->attribute->get("twofactor", "state") == '' && $this->getConf("optinout") == 'optout'; 186*fca58076SAndreas Gohr // The user must login if wiki mandatory is on or if the user is logged in and user is opt in. 187*fca58076SAndreas Gohr $must_login = $mandatory || ($this->attribute->get("twofactor", 188*fca58076SAndreas Gohr "state") == 'in' && $INPUT->server->str('REMOTE_USER', '')); 189*fca58076SAndreas Gohr $has_clearance = $this->get_clearance() === true; 190*fca58076SAndreas Gohr $this->log('twofactor_action_process_handler: USERINFO: ' . print_r($USERINFO, true), self::LOGGING_DEBUGPLUS); 191*fca58076SAndreas Gohr 192*fca58076SAndreas Gohr // Possible combination skipped- not logged in and 2FA is not requred for user {optout conf or (no selection and optin conf)}. 193*fca58076SAndreas Gohr 194*fca58076SAndreas Gohr // Check to see if updating twofactor is required. 195*fca58076SAndreas Gohr // This happens if the wiki is mandatory, the user has not opted out of an opt-out wiki, or if the user has opted in, and if there are no available mods for use. 196*fca58076SAndreas Gohr // The user cannot have available mods without setting them up, and cannot unless the wiki is mandatory or the user has opted in. 197*fca58076SAndreas Gohr if (($must_login || $not_opted_out) && !$available) { 198*fca58076SAndreas Gohr // If the user has not been granted access at this point, do so or they will get booted after setting up 2FA. 199*fca58076SAndreas Gohr if (!$has_clearance) { 200*fca58076SAndreas Gohr $this->_grant_clearance(); 201*fca58076SAndreas Gohr } 202*fca58076SAndreas Gohr // We need to go to the twofactor profile. 203*fca58076SAndreas Gohr // If we were setup properly, we would not be here in the code. 204*fca58076SAndreas Gohr $event->preventDefault(); 205*fca58076SAndreas Gohr $event->stopPropagation(); 206*fca58076SAndreas Gohr $event->result = false; 207*fca58076SAndreas Gohr // Send loglog an event to show the user aborted 2FA. 208*fca58076SAndreas Gohr $log = array('message' => 'logged in, ' . $this->getLang('2fa_mandatory'), 'user' => $user); 209*fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 210*fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => 'twofactor_profile'), true, '&')); 211*fca58076SAndreas Gohr return; 212*fca58076SAndreas Gohr } 213*fca58076SAndreas Gohr 214*fca58076SAndreas Gohr // Now validate login before proceeding. 215*fca58076SAndreas Gohr if (!$has_clearance) { 216*fca58076SAndreas Gohr if ($must_login) { 217*fca58076SAndreas Gohr if (!in_array($event->data, array('login', 'twofactor_login'))) { 218*fca58076SAndreas Gohr // If not logged in then force to the login page. 219*fca58076SAndreas Gohr $event->preventDefault(); 220*fca58076SAndreas Gohr $event->stopPropagation(); 221*fca58076SAndreas Gohr $event->result = false; 222*fca58076SAndreas Gohr // If there are OTP generators, then use them. 223*fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => 'twofactor_login'), true, '&')); 224*fca58076SAndreas Gohr return; 225*fca58076SAndreas Gohr } 226*fca58076SAndreas Gohr // Otherwise go to where we are told. 227*fca58076SAndreas Gohr return; 228*fca58076SAndreas Gohr } 229*fca58076SAndreas Gohr // The user is not set with 2FA and is not required to. 230*fca58076SAndreas Gohr // Grant clearance and continue. 231*fca58076SAndreas Gohr $this->_grant_clearance(); 232*fca58076SAndreas Gohr } 233*fca58076SAndreas Gohr // Otherwise everything is good! 234*fca58076SAndreas Gohr return; 235*fca58076SAndreas Gohr } 236*fca58076SAndreas Gohr 237*fca58076SAndreas Gohr 238*fca58076SAndreas Gohr public function twofactor_handle_unknown_action(Doku_Event $event, $param) 239*fca58076SAndreas Gohr { 240*fca58076SAndreas Gohr if ($event->data == 'twofactor_login') { 241*fca58076SAndreas Gohr $event->preventDefault(); 242*fca58076SAndreas Gohr $event->stopPropagation(); 243*fca58076SAndreas Gohr $event->result = $this->twofactor_otp_login($event, $param); 244*fca58076SAndreas Gohr return; 245*fca58076SAndreas Gohr } 246*fca58076SAndreas Gohr } 247*fca58076SAndreas Gohr 248*fca58076SAndreas Gohr public function twofactor_get_unknown_action(&$event, $param) 249*fca58076SAndreas Gohr { 250*fca58076SAndreas Gohr $this->log('start: twofactor_before_auth_check', self::LOGGING_DEBUG); 251*fca58076SAndreas Gohr switch ($event->data['type']) { 252*fca58076SAndreas Gohr case 'twofactor_profile': 253*fca58076SAndreas Gohr $event->data['params'] = array('do' => 'twofactor_profile'); 254*fca58076SAndreas Gohr // Inject text into $lang. 255*fca58076SAndreas Gohr global $lang; 256*fca58076SAndreas Gohr $lang['btn_twofactor_profile'] = $this->getLang('btn_twofactor_profile'); 257*fca58076SAndreas Gohr $event->preventDefault(); 258*fca58076SAndreas Gohr $event->stopPropagation(); 259*fca58076SAndreas Gohr $event->result = false; 260*fca58076SAndreas Gohr break; 261*fca58076SAndreas Gohr } 262*fca58076SAndreas Gohr } 263*fca58076SAndreas Gohr 264*fca58076SAndreas Gohr /** 265*fca58076SAndreas Gohr * Logout this session from two factor authentication. Purge any existing 266*fca58076SAndreas Gohr * OTP from the user's attributes. 267*fca58076SAndreas Gohr */ 268*fca58076SAndreas Gohr private function _logout() 269*fca58076SAndreas Gohr { 270*fca58076SAndreas Gohr global $conf, $INPUT; 271*fca58076SAndreas Gohr $this->log('_logout: start', self::LOGGING_DEBUG); 272*fca58076SAndreas Gohr $this->log(print_r(array($_SESSION, $_COOKIE), true), self::LOGGING_DEBUGPLUS); 273*fca58076SAndreas Gohr // No need to do this as long as no Cookie or session for login is present! 274*fca58076SAndreas Gohr if (empty($_SESSION[DOKU_COOKIE]['twofactor_clearance']) && empty($_COOKIE[TWOFACTOR_COOKIE])) { 275*fca58076SAndreas Gohr $this->log('_logout: quitting, no cookies', self::LOGGING_DEBUG); 276*fca58076SAndreas Gohr return; 277*fca58076SAndreas Gohr } 278*fca58076SAndreas Gohr // Audit log. 279*fca58076SAndreas Gohr $this->log("2FA Logout: " . $INPUT->server->str('REMOTE_USER', $_REQUEST['r']), self::LOGGING_AUDIT); 280*fca58076SAndreas Gohr if ($this->attribute) { 281*fca58076SAndreas Gohr // Purge outstanding OTPs. 282*fca58076SAndreas Gohr $this->attribute->del("twofactor", "otp"); 283*fca58076SAndreas Gohr // Purge cookie and session ID relation. 284*fca58076SAndreas Gohr $key = $_COOKIE[TWOFACTOR_COOKIE]; 285*fca58076SAndreas Gohr if (!empty($key) && substr($key, 0, 3) != 'id.') { 286*fca58076SAndreas Gohr $id = $this->attribute->del("twofactor", $key); 287*fca58076SAndreas Gohr } 288*fca58076SAndreas Gohr // Wipe out 2FA cookie. 289*fca58076SAndreas Gohr $this->log('del cookies: ' . TWOFACTOR_COOKIE . ' ' . print_r(headers_sent(), true), 290*fca58076SAndreas Gohr self::LOGGING_DEBUGPLUS); 291*fca58076SAndreas Gohr $cookie = ''; 292*fca58076SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 293*fca58076SAndreas Gohr $time = time() - 600000; //many seconds ago 294*fca58076SAndreas Gohr setcookie(TWOFACTOR_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 295*fca58076SAndreas Gohr unset($_COOKIE[TWOFACTOR_COOKIE]); 296*fca58076SAndreas Gohr // Just in case, unset the setTime flag so attributes will be saved again. 297*fca58076SAndreas Gohr $this->setTime = false; 298*fca58076SAndreas Gohr } 299*fca58076SAndreas Gohr // Before we get here, the session is closed. Reopen it to logout the user. 300*fca58076SAndreas Gohr if (!headers_sent()) { 301*fca58076SAndreas Gohr $session = session_status() != PHP_SESSION_NONE; 302*fca58076SAndreas Gohr if (!$session) { 303*fca58076SAndreas Gohr session_start(); 304*fca58076SAndreas Gohr } 305*fca58076SAndreas Gohr $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = false; 306*fca58076SAndreas Gohr unset($_SESSION[DOKU_COOKIE]['twofactor_clearance']); 307*fca58076SAndreas Gohr if (!$session) { 308*fca58076SAndreas Gohr session_write_close(); 309*fca58076SAndreas Gohr } 310*fca58076SAndreas Gohr } else { 311*fca58076SAndreas Gohr msg("Error! You have not been logged off!!!", -1); 312*fca58076SAndreas Gohr } 313*fca58076SAndreas Gohr } 314*fca58076SAndreas Gohr 315*fca58076SAndreas Gohr /** 316*fca58076SAndreas Gohr * See if the current session has passed two factor authentication. 317*fca58076SAndreas Gohr * @return bool - true if the session as successfully passed two factor 318*fca58076SAndreas Gohr * authentication. 319*fca58076SAndreas Gohr */ 320*fca58076SAndreas Gohr public function get_clearance($user = null) 321*fca58076SAndreas Gohr { 322*fca58076SAndreas Gohr global $INPUT; 323*fca58076SAndreas Gohr $this->log("get_clearance: start", self::LOGGING_DEBUG); 324*fca58076SAndreas Gohr $this->log("User:" . $INPUT->server->str('REMOTE_USER', null), self::LOGGING_DEBUGPLUS); 325*fca58076SAndreas Gohr # Get and correct the refresh expiry. 326*fca58076SAndreas Gohr # At least 5 min, at most 1440 min (1 day). 327*fca58076SAndreas Gohr $refreshexpiry = min(max($this->getConf('refreshexpiry'), 5), 1400) * 60; 328*fca58076SAndreas Gohr # First check if we have a key. No key === no login. 329*fca58076SAndreas Gohr $key = $_COOKIE[TWOFACTOR_COOKIE]; 330*fca58076SAndreas Gohr if (empty($key)) { 331*fca58076SAndreas Gohr $this->log("get_clearance: No cookie.", self::LOGGING_DEBUGPLUS); 332*fca58076SAndreas Gohr return false; 333*fca58076SAndreas Gohr } 334*fca58076SAndreas Gohr # If the key is not valid, logout. 335*fca58076SAndreas Gohr if (substr($key, 0, 3) != 'id.') { 336*fca58076SAndreas Gohr $this->log("get_clearance: BAD cookie.", self::LOGGING_DEBUGPLUS); 337*fca58076SAndreas Gohr // Purge the login data just in case. 338*fca58076SAndreas Gohr $this->_logout(); 339*fca58076SAndreas Gohr return false; 340*fca58076SAndreas Gohr } 341*fca58076SAndreas Gohr # Load the expiry value from session. 342*fca58076SAndreas Gohr $expiry = $_SESSION[DOKU_COOKIE]['twofactor_clearance']; 343*fca58076SAndreas Gohr # Check if this time is valid. 344*fca58076SAndreas Gohr $clearance = (!empty($expiry) && $expiry + $refreshexpiry > time()); 345*fca58076SAndreas Gohr if (!$clearance) { 346*fca58076SAndreas Gohr # First use this time to purge the old IDs from attribute. 347*fca58076SAndreas Gohr foreach (array_filter($this->attribute->enumerateAttributes("twofactor", $user), function ($key) { 348*fca58076SAndreas Gohr substr($key, 0, 3) == 'id.'; 349*fca58076SAndreas Gohr }) as $attr) { 350*fca58076SAndreas Gohr if ($this->attribute->get("twofactor", $attr, $user) + $refreshexpiry < time()) { 351*fca58076SAndreas Gohr $this->attribute->del("twofactor", $attr, $user); 352*fca58076SAndreas Gohr } 353*fca58076SAndreas Gohr } 354*fca58076SAndreas Gohr # Check if this key still exists. 355*fca58076SAndreas Gohr $clearance = $this->attribute->exists("twofactor", $key, $user); 356*fca58076SAndreas Gohr if ($clearance) { 357*fca58076SAndreas Gohr $this->log("get_clearance: 2FA revived by cookie. Expiry: " . print_r($expiry, 358*fca58076SAndreas Gohr true) . " Session: " . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS); 359*fca58076SAndreas Gohr } 360*fca58076SAndreas Gohr } 361*fca58076SAndreas Gohr if ($clearance && !$this->setTime) { 362*fca58076SAndreas Gohr $session = session_status() != PHP_SESSION_NONE; 363*fca58076SAndreas Gohr if (!$session) { 364*fca58076SAndreas Gohr session_start(); 365*fca58076SAndreas Gohr } 366*fca58076SAndreas Gohr $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = time(); 367*fca58076SAndreas Gohr if (!$session) { 368*fca58076SAndreas Gohr session_write_close(); 369*fca58076SAndreas Gohr } 370*fca58076SAndreas Gohr $this->attribute->set("twofactor", $key, $_SESSION[DOKU_COOKIE]['twofactor_clearance'], $user); 371*fca58076SAndreas Gohr // Set this flag to stop future updates. 372*fca58076SAndreas Gohr $this->setTime = true; 373*fca58076SAndreas Gohr $this->log("get_clearance: Session reset. Session: " . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS); 374*fca58076SAndreas Gohr } elseif (!$clearance) { 375*fca58076SAndreas Gohr // Otherwise logout. 376*fca58076SAndreas Gohr $this->_logout(); 377*fca58076SAndreas Gohr } 378*fca58076SAndreas Gohr return $clearance; 379*fca58076SAndreas Gohr } 380*fca58076SAndreas Gohr 381*fca58076SAndreas Gohr /** 382*fca58076SAndreas Gohr * Flags this session as having passed two factor authentication. 383*fca58076SAndreas Gohr * @return bool - returns true on successfully granting two factor clearance. 384*fca58076SAndreas Gohr */ 385*fca58076SAndreas Gohr private function _grant_clearance($user = null) 386*fca58076SAndreas Gohr { 387*fca58076SAndreas Gohr global $conf, $INPUT; 388*fca58076SAndreas Gohr $this->log("_grant_clearance: start", self::LOGGING_DEBUG); 389*fca58076SAndreas Gohr $this->log('2FA Login: ' . $INPUT->server->str("REMOTE_USER", $user), self::LOGGING_AUDIT); 390*fca58076SAndreas Gohr if ($INPUT->server->str("REMOTE_USER", $user) == 1) { 391*fca58076SAndreas Gohr $this->log("_grant_clearance: start", self::LOGGING_DEBUGPLUS); 392*fca58076SAndreas Gohr } 393*fca58076SAndreas Gohr // Purge the otp code as a security measure. 394*fca58076SAndreas Gohr $this->attribute->del("twofactor", "otp", $user); 395*fca58076SAndreas Gohr if (!headers_sent()) { 396*fca58076SAndreas Gohr $session = session_status() != PHP_SESSION_NONE; 397*fca58076SAndreas Gohr if (!$session) { 398*fca58076SAndreas Gohr session_start(); 399*fca58076SAndreas Gohr } 400*fca58076SAndreas Gohr $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = time(); 401*fca58076SAndreas Gohr // Set the notify flag if set or required by wiki. 402*fca58076SAndreas Gohr $this->log('_grant_clearance: conf:' . $this->getConf('loginnotice') . ' user:' . ($this->attribute->get("twofactor", 403*fca58076SAndreas Gohr "loginnotice", $user) === true ? 'true' : 'false'), self::LOGGING_DEBUG); 404*fca58076SAndreas Gohr $send_wanted = $this->getConf('loginnotice') == 'always' || ($this->getConf('loginnotice') == 'user' && $this->attribute->get("twofactor", 405*fca58076SAndreas Gohr "loginnotice", $user) == true); 406*fca58076SAndreas Gohr if ($send_wanted) { 407*fca58076SAndreas Gohr $_SESSION[DOKU_COOKIE]['twofactor_notify'] = true; 408*fca58076SAndreas Gohr } 409*fca58076SAndreas Gohr if (!$session) { 410*fca58076SAndreas Gohr session_write_close(); 411*fca58076SAndreas Gohr } 412*fca58076SAndreas Gohr } else { 413*fca58076SAndreas Gohr msg("Error! You have not been logged in!!!", -1); 414*fca58076SAndreas Gohr } 415*fca58076SAndreas Gohr // Creating a cookie in case the session purges. 416*fca58076SAndreas Gohr $key = 'id.' . session_id(); 417*fca58076SAndreas Gohr // Storing a timeout value. 418*fca58076SAndreas Gohr $this->attribute->set("twofactor", $key, $_SESSION[DOKU_COOKIE]['twofactor_clearance'], $user); 419*fca58076SAndreas Gohr // Set the 2FA cookie. 420*fca58076SAndreas Gohr $this->log('_grant_clearance: new cookies: ' . TWOFACTOR_COOKIE . ' ' . print_r(headers_sent(), true), 421*fca58076SAndreas Gohr self::LOGGING_DEBUGPLUS); 422*fca58076SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 423*fca58076SAndreas Gohr $time = time() + 60 * 60 * 24 * 365; //one year 424*fca58076SAndreas Gohr setcookie(TWOFACTOR_COOKIE, $key, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 425*fca58076SAndreas Gohr $_COOKIE[TWOFACTOR_COOKIE] = $key; 426*fca58076SAndreas Gohr return !empty($_SESSION[DOKU_COOKIE]['twofactor_clearance']); 427*fca58076SAndreas Gohr } 428*fca58076SAndreas Gohr 429*fca58076SAndreas Gohr /** 430*fca58076SAndreas Gohr * Sends emails notifying user of successfult 2FA login. 431*fca58076SAndreas Gohr * @return mixed - returns true on successfully sending notification to all 432*fca58076SAndreas Gohr * modules, false if no notifications were sent, or a number indicating 433*fca58076SAndreas Gohr * the number of modules that suceeded. 434*fca58076SAndreas Gohr */ 435*fca58076SAndreas Gohr private function _send_login_notification() 436*fca58076SAndreas Gohr { 437*fca58076SAndreas Gohr $this->log("_send_login_notification: start", self::LOGGING_DEBUG); 438*fca58076SAndreas Gohr // Send login notification. 439*fca58076SAndreas Gohr $module = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor", 440*fca58076SAndreas Gohr "defaultmod") : null; 441*fca58076SAndreas Gohr $subject = $this->getConf('loginsubject'); 442*fca58076SAndreas Gohr $time = date(DATE_RFC2822); 443*fca58076SAndreas Gohr $message = str_replace('$time', $time, $this->getConf('logincontent')); 444*fca58076SAndreas Gohr $result = $this->_send_message($subject, $message, $module); 445*fca58076SAndreas Gohr return $result; 446*fca58076SAndreas Gohr } 447*fca58076SAndreas Gohr 448*fca58076SAndreas Gohr /** 449*fca58076SAndreas Gohr * Handles the authentication check. Screens Google Authenticator OTP, if available. 450*fca58076SAndreas Gohr * NOTE: NOT LOGGED IN YET. Attribute requires user name. 451*fca58076SAndreas Gohr */ 452*fca58076SAndreas Gohr function twofactor_before_auth_check(&$event, $param) 453*fca58076SAndreas Gohr { 454*fca58076SAndreas Gohr global $ACT, $INPUT; 455*fca58076SAndreas Gohr $this->log("twofactor_before_auth_check: start $ACT", self::LOGGING_DEBUG); 456*fca58076SAndreas Gohr $this->log("twofactor_before_auth_check: Cookie: " . print_r($_COOKIE, true), self::LOGGING_DEBUGPLUS); 457*fca58076SAndreas Gohr // Only operate if this is a login. 458*fca58076SAndreas Gohr if ($ACT !== 'login') { 459*fca58076SAndreas Gohr return; 460*fca58076SAndreas Gohr } 461*fca58076SAndreas Gohr // If there is no supplied username, then there is nothing to check at this time. 462*fca58076SAndreas Gohr if (!$event->data['user']) { 463*fca58076SAndreas Gohr return; 464*fca58076SAndreas Gohr } 465*fca58076SAndreas Gohr $user = $INPUT->server->str('REMOTE_USER', $event->data['user']); 466*fca58076SAndreas Gohr // Set helper variables here. 467*fca58076SAndreas Gohr $this->_setHelperVariables($user); 468*fca58076SAndreas Gohr // If the user still has clearance, then we can skip this. 469*fca58076SAndreas Gohr if ($this->get_clearance($user)) { 470*fca58076SAndreas Gohr return; 471*fca58076SAndreas Gohr } 472*fca58076SAndreas Gohr // Allow the user to try to use login tokens, even if the account cannot use them. 473*fca58076SAndreas Gohr $otp = $INPUT->str('otp', ''); 474*fca58076SAndreas Gohr if ($otp !== '') { 475*fca58076SAndreas Gohr // Check for any modules that support OTP at login and are ready for use. 476*fca58076SAndreas Gohr foreach ($this->tokenMods as $mod) { 477*fca58076SAndreas Gohr $result = $mod->processLogin($otp, $user); 478*fca58076SAndreas Gohr if ($result) { 479*fca58076SAndreas Gohr // The OTP code was valid. 480*fca58076SAndreas Gohr $this->_grant_clearance($user); 481*fca58076SAndreas Gohr // Send loglog an event to show the user logged in using a token. 482*fca58076SAndreas Gohr $log = array('message' => 'logged in ' . $this->getLang('token_ok'), 'user' => $user); 483*fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 484*fca58076SAndreas Gohr return; 485*fca58076SAndreas Gohr } 486*fca58076SAndreas Gohr } 487*fca58076SAndreas Gohr global $lang; 488*fca58076SAndreas Gohr msg($lang['badlogin'], -1); 489*fca58076SAndreas Gohr $event->preventDefault(); 490*fca58076SAndreas Gohr $event->result = false; 491*fca58076SAndreas Gohr // Send loglog an event to show the failure 492*fca58076SAndreas Gohr if (count($this->tokenMods) == 0) { 493*fca58076SAndreas Gohr $log = array('message' => 'failed ' . $this->getLang('no_tokens'), 'user' => $user); 494*fca58076SAndreas Gohr } else { 495*fca58076SAndreas Gohr $log = array('message' => 'failed ' . $this->getLang('token_mismatch'), 'user' => $user); 496*fca58076SAndreas Gohr } 497*fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 498*fca58076SAndreas Gohr return; 499*fca58076SAndreas Gohr } 500*fca58076SAndreas Gohr // No GA OTP was supplied. 501*fca58076SAndreas Gohr // If the user has no modules available, then grant access. 502*fca58076SAndreas Gohr // The action preprocessing will send the user to the profile if needed. 503*fca58076SAndreas Gohr $available = count($this->tokenMods) + count($this->otpMods) > 0; 504*fca58076SAndreas Gohr $this->log('twofactor_before_auth_check: Tokens:' . count($this->tokenMods) . ' Codes:' . count($this->otpMods) . " Available:" . (int)$available, 505*fca58076SAndreas Gohr self::LOGGING_DEBUGPLUS); 506*fca58076SAndreas Gohr if (!$available) { 507*fca58076SAndreas Gohr // The user could not authenticate if they wanted to. 508*fca58076SAndreas Gohr // Set this so they don't get auth prompted while setting up 2FA. 509*fca58076SAndreas Gohr $this->_grant_clearance($user); 510*fca58076SAndreas Gohr return; 511*fca58076SAndreas Gohr } 512*fca58076SAndreas Gohr // At this point, the user has a working module. 513*fca58076SAndreas Gohr // If the only working module is for a token, then fail. 514*fca58076SAndreas Gohr if (count($this->otpMods) == 0) { 515*fca58076SAndreas Gohr msg($this->getLang('mustusetoken'), -1); 516*fca58076SAndreas Gohr $event->preventDefault(); 517*fca58076SAndreas Gohr return; 518*fca58076SAndreas Gohr } 519*fca58076SAndreas Gohr // The user is logged in to auth, but not into twofactor. 520*fca58076SAndreas Gohr // The redirection handler will send the user to the twofactor login. 521*fca58076SAndreas Gohr return; 522*fca58076SAndreas Gohr } 523*fca58076SAndreas Gohr 524*fca58076SAndreas Gohr /** 525*fca58076SAndreas Gohr * @param $event 526*fca58076SAndreas Gohr * @param $param 527*fca58076SAndreas Gohr */ 528*fca58076SAndreas Gohr function twofactor_after_auth_check(&$event, $param) 529*fca58076SAndreas Gohr { 530*fca58076SAndreas Gohr global $ACT; 531*fca58076SAndreas Gohr global $INPUT; 532*fca58076SAndreas Gohr $this->log("twofactor_after_auth_check: start", self::LOGGING_DEBUG); 533*fca58076SAndreas Gohr // Check if the action was login. 534*fca58076SAndreas Gohr if ($ACT == 'login') { 535*fca58076SAndreas Gohr // If there *was* no one logged in, then purge 2FA tokens. 536*fca58076SAndreas Gohr if ($INPUT->server->str('REMOTE_USER', '') == '') { 537*fca58076SAndreas Gohr $this->_logout(); 538*fca58076SAndreas Gohr // If someone *just* logged in, then fire off a log. 539*fca58076SAndreas Gohr if ($event->data['user']) { 540*fca58076SAndreas Gohr // Send loglog an event to show the user logged in but needs OTP code. 541*fca58076SAndreas Gohr $log = array( 542*fca58076SAndreas Gohr 'message' => 'logged in, ' . $this->getLang('requires_otp'), 543*fca58076SAndreas Gohr 'user' => $event->data['user'], 544*fca58076SAndreas Gohr ); 545*fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 546*fca58076SAndreas Gohr } 547*fca58076SAndreas Gohr return; 548*fca58076SAndreas Gohr } 549*fca58076SAndreas Gohr } 550*fca58076SAndreas Gohr // Update helper variables here since we are logged in. 551*fca58076SAndreas Gohr $this->_setHelperVariables(); 552*fca58076SAndreas Gohr // If set, then send login notification and clear flag. 553*fca58076SAndreas Gohr if ($_SESSION[DOKU_COOKIE]['twofactor_notify'] == true) { 554*fca58076SAndreas Gohr // Set the clear flag if no messages can be sent or if the result is not false. 555*fca58076SAndreas Gohr $clear = count($this_ > otpMods) > 0 || $this->_send_login_notification() !== false; 556*fca58076SAndreas Gohr if ($clear) { 557*fca58076SAndreas Gohr unset($_SESSION[DOKU_COOKIE]['twofactor_notify']); 558*fca58076SAndreas Gohr } 559*fca58076SAndreas Gohr } 560*fca58076SAndreas Gohr return; 561*fca58076SAndreas Gohr } 562*fca58076SAndreas Gohr 563*fca58076SAndreas Gohr /* Returns action to take. */ 564*fca58076SAndreas Gohr private function _process_otp(&$event, $param) 565*fca58076SAndreas Gohr { 566*fca58076SAndreas Gohr global $ACT, $ID, $INPUT; 567*fca58076SAndreas Gohr $this->log("_process_otp: start", self::LOGGING_DEBUG); 568*fca58076SAndreas Gohr // Get the logged in user. 569*fca58076SAndreas Gohr $user = $INPUT->server->str('REMOTE_USER'); 570*fca58076SAndreas Gohr // See if the user is quitting OTP. We don't call it logoff because we don't want the user to think they are logged in! 571*fca58076SAndreas Gohr // This has to be checked before the template is started. 572*fca58076SAndreas Gohr if ($INPUT->has('otpquit')) { 573*fca58076SAndreas Gohr // Send loglog an event to show the user aborted 2FA. 574*fca58076SAndreas Gohr $log = array('message' => 'logged off, ' . $this->getLang('quit_otp'), 'user' => $user); 575*fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 576*fca58076SAndreas Gohr // Redirect to logout. 577*fca58076SAndreas Gohr return 'logout'; 578*fca58076SAndreas Gohr } 579*fca58076SAndreas Gohr // Check if the user asked to generate and resend the OTP. 580*fca58076SAndreas Gohr if ($INPUT->has('resend')) { 581*fca58076SAndreas Gohr if ($INPUT->has('useall')) { 582*fca58076SAndreas Gohr $defaultMod = null; 583*fca58076SAndreas Gohr } else { 584*fca58076SAndreas Gohr $defaultMod = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor", 585*fca58076SAndreas Gohr "defaultmod") : null; 586*fca58076SAndreas Gohr } 587*fca58076SAndreas Gohr // At this point, try to send the OTP. 588*fca58076SAndreas Gohr $mod = array_key_exists($defaultMod, $this->otpMods) ? $this->otpMods[$defaultMod] : null; 589*fca58076SAndreas Gohr $this->_send_otp($mod); 590*fca58076SAndreas Gohr return; 591*fca58076SAndreas Gohr } 592*fca58076SAndreas Gohr // If a OTP has been submitted by the user, then verify the OTP. 593*fca58076SAndreas Gohr // If verified, then grant clearance and continue normally. 594*fca58076SAndreas Gohr $otp = $INPUT->str('otpcode'); 595*fca58076SAndreas Gohr if ($otp) { 596*fca58076SAndreas Gohr foreach ($this->otpMods as $mod) { 597*fca58076SAndreas Gohr $result = $mod->processLogin($otp); 598*fca58076SAndreas Gohr if ($result) { 599*fca58076SAndreas Gohr // The OTP code was valid. 600*fca58076SAndreas Gohr $this->_grant_clearance(); 601*fca58076SAndreas Gohr // Send loglog an event to show the user passed 2FA. 602*fca58076SAndreas Gohr $log = array('message' => 'logged in ' . $this->getLang('otp_ok'), 'user' => $user); 603*fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 604*fca58076SAndreas Gohr /* 605*fca58076SAndreas Gohr // This bypasses sending any further events to other modules for the login we stole earlier. 606*fca58076SAndreas Gohr return 'show'; 607*fca58076SAndreas Gohr */ 608*fca58076SAndreas Gohr // This will trigger the login events again. However, this is to ensure 609*fca58076SAndreas Gohr // that other modules work correctly because we hijacked this event earlier. 610*fca58076SAndreas Gohr return 'login'; 611*fca58076SAndreas Gohr } 612*fca58076SAndreas Gohr } 613*fca58076SAndreas Gohr // Send loglog an event to show the user entered the wrong OTP code. 614*fca58076SAndreas Gohr $log = array('message' => 'failed OTP login, ' . $this->getLang('otp_mismatch'), 'user' => $user); 615*fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 616*fca58076SAndreas Gohr msg($this->getLang('twofactor_invalidotp'), -1); 617*fca58076SAndreas Gohr } 618*fca58076SAndreas Gohr return; 619*fca58076SAndreas Gohr } 620*fca58076SAndreas Gohr 621*fca58076SAndreas Gohr /** 622*fca58076SAndreas Gohr * Process any updates to two factor settings. 623*fca58076SAndreas Gohr */ 624*fca58076SAndreas Gohr private function _process_changes(&$event, $param) 625*fca58076SAndreas Gohr { 626*fca58076SAndreas Gohr // If the plugin is disabled, then exit. 627*fca58076SAndreas Gohr $this->log("_process_changes: start", self::LOGGING_DEBUG); 628*fca58076SAndreas Gohr $changed = false; 629*fca58076SAndreas Gohr global $INPUT, $USERINFO, $conf, $auth, $lang, $ACT; 630*fca58076SAndreas Gohr if (!$INPUT->has('save')) { 631*fca58076SAndreas Gohr return; 632*fca58076SAndreas Gohr } 633*fca58076SAndreas Gohr // In needed, verify password. 634*fca58076SAndreas Gohr if ($conf['profileconfirm']) { 635*fca58076SAndreas Gohr if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) { 636*fca58076SAndreas Gohr msg($lang['badpassconfirm'], -1); 637*fca58076SAndreas Gohr return; 638*fca58076SAndreas Gohr } 639*fca58076SAndreas Gohr } 640*fca58076SAndreas Gohr // Process opt in/out. 641*fca58076SAndreas Gohr if ($this->getConf("optinout") != 'mandatory') { 642*fca58076SAndreas Gohr $oldoptinout = $this->attribute->get("twofactor", "state"); 643*fca58076SAndreas Gohr $optinout = $INPUT->bool('optinout', false) ? 'in' : 'out'; 644*fca58076SAndreas Gohr if ($oldoptinout != $optinout) { 645*fca58076SAndreas Gohr $this->attribute->set("twofactor", "state", $optinout); 646*fca58076SAndreas Gohr $changed = true; 647*fca58076SAndreas Gohr } 648*fca58076SAndreas Gohr } 649*fca58076SAndreas Gohr // Process notifications. 650*fca58076SAndreas Gohr if ($this->getConf("loginnotice") == 'user') { 651*fca58076SAndreas Gohr $oldloginnotice = $this->attribute->get("twofactor", "loginnotice"); 652*fca58076SAndreas Gohr $loginnotice = $INPUT->bool('loginnotice', false); 653*fca58076SAndreas Gohr if ($oldloginnotice != $loginnotice) { 654*fca58076SAndreas Gohr $this->attribute->set("twofactor", "loginnotice", $loginnotice); 655*fca58076SAndreas Gohr $changed = true; 656*fca58076SAndreas Gohr } 657*fca58076SAndreas Gohr } 658*fca58076SAndreas Gohr // Process default module. 659*fca58076SAndreas Gohr $defaultmodule = $INPUT->str('default_module', ''); 660*fca58076SAndreas Gohr if ($defaultmodule) { 661*fca58076SAndreas Gohr if ($defaultmodule === $this->getLang('useallotp')) { 662*fca58076SAndreas Gohr // Set to use ALL OTP channels. 663*fca58076SAndreas Gohr $this->attribute->set("twofactor", "defaultmod", null); 664*fca58076SAndreas Gohr $changed = true; 665*fca58076SAndreas Gohr } else { 666*fca58076SAndreas Gohr $useableMods = array(); 667*fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 668*fca58076SAndreas Gohr if (!$mod->canAuthLogin() && $mod->canUse()) { 669*fca58076SAndreas Gohr $useableMods[$mod->getLang("name")] = $mod; 670*fca58076SAndreas Gohr } 671*fca58076SAndreas Gohr } 672*fca58076SAndreas Gohr if (array_key_exists($defaultmodule, $useableMods)) { 673*fca58076SAndreas Gohr $this->attribute->set("twofactor", "defaultmod", $defaultmodule); 674*fca58076SAndreas Gohr $changed = true; 675*fca58076SAndreas Gohr } 676*fca58076SAndreas Gohr } 677*fca58076SAndreas Gohr } 678*fca58076SAndreas Gohr // Update module settings. 679*fca58076SAndreas Gohr $sendotp = null; 680*fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 681*fca58076SAndreas Gohr $this->log('_process_changes: processing ' . get_class($mod) . '::processProfileForm()', 682*fca58076SAndreas Gohr self::LOGGING_DEBUG); 683*fca58076SAndreas Gohr $result = $mod->processProfileForm(); 684*fca58076SAndreas Gohr $this->log('_process_changes: processing ' . get_class($mod) . '::processProfileForm() == ' . $result, 685*fca58076SAndreas Gohr self::LOGGING_DEBUGPLUS); 686*fca58076SAndreas Gohr // false:change failed 'failed':OTP failed null: no change made 687*fca58076SAndreas Gohr $changed |= $result !== false && $result !== 'failed' && $result !== null; 688*fca58076SAndreas Gohr switch ((string)$result) { 689*fca58076SAndreas Gohr case 'verified': 690*fca58076SAndreas Gohr // Remove used OTP. 691*fca58076SAndreas Gohr $this->attribute->del("twofactor", "otp"); 692*fca58076SAndreas Gohr msg($mod->getLang('passedsetup'), 1); 693*fca58076SAndreas Gohr // Reset helper variables. 694*fca58076SAndreas Gohr $this->_setHelperVariables(); 695*fca58076SAndreas Gohr $this->log("2FA Added: " . $INPUT->server->str('REMOTE_USER', '') . ' ' . get_class($mod), 696*fca58076SAndreas Gohr self::LOGGING_AUDIT); 697*fca58076SAndreas Gohr break; 698*fca58076SAndreas Gohr case 'failed': 699*fca58076SAndreas Gohr msg($mod->getLang('failedsetup'), -1); 700*fca58076SAndreas Gohr break; 701*fca58076SAndreas Gohr case 'otp': 702*fca58076SAndreas Gohr if (!$sendotp) { 703*fca58076SAndreas Gohr $sendotp = $mod; 704*fca58076SAndreas Gohr } 705*fca58076SAndreas Gohr break; 706*fca58076SAndreas Gohr case 'deleted': 707*fca58076SAndreas Gohr $this->log("2FA Removed: " . $INPUT->server->str('REMOTE_USER', '') . ' ' . get_class($mod), 708*fca58076SAndreas Gohr self::LOGGING_AUDIT); 709*fca58076SAndreas Gohr // Reset helper variables. 710*fca58076SAndreas Gohr $this->_setHelperVariables(); 711*fca58076SAndreas Gohr break; 712*fca58076SAndreas Gohr } 713*fca58076SAndreas Gohr } 714*fca58076SAndreas Gohr // Send OTP if requested. 715*fca58076SAndreas Gohr if (is_object($sendotp)) { 716*fca58076SAndreas Gohr // Force the message since it will fail the canUse function. 717*fca58076SAndreas Gohr if ($this->_send_otp($sendotp, true)) { 718*fca58076SAndreas Gohr msg($sendotp->getLang('needsetup'), 1); 719*fca58076SAndreas Gohr } else { 720*fca58076SAndreas Gohr msg("Could not send message using " . get_class($sendotp), -1); 721*fca58076SAndreas Gohr } 722*fca58076SAndreas Gohr } 723*fca58076SAndreas Gohr // Update change status if changed. 724*fca58076SAndreas Gohr if ($changed) { 725*fca58076SAndreas Gohr // If there were any changes, update the available tokens accordingly. 726*fca58076SAndreas Gohr $this->_setHelperVariables(); 727*fca58076SAndreas Gohr msg($this->getLang('updated'), 1); 728*fca58076SAndreas Gohr } 729*fca58076SAndreas Gohr return true; 730*fca58076SAndreas Gohr } 731*fca58076SAndreas Gohr 732*fca58076SAndreas Gohr /** 733*fca58076SAndreas Gohr * Handles the email and text OTP options. 734*fca58076SAndreas Gohr * NOTE: The user will be technically logged in at this point. This module will rewrite the 735*fca58076SAndreas Gohr * page with the prompt for the OTP until validated or the user logs out. 736*fca58076SAndreas Gohr */ 737*fca58076SAndreas Gohr function twofactor_otp_login(&$event, $param) 738*fca58076SAndreas Gohr { 739*fca58076SAndreas Gohr $this->log("twofactor_otp_login: start", self::LOGGING_DEBUG); 740*fca58076SAndreas Gohr // Skip this if not logged in or already two factor authenticated. 741*fca58076SAndreas Gohr // Ensure the OTP exists and is still valid. If we need to, send a OTP. 742*fca58076SAndreas Gohr $otpQuery = $this->get_otp_code(); 743*fca58076SAndreas Gohr if ($otpQuery == false) { 744*fca58076SAndreas Gohr $useableMods = array(); 745*fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 746*fca58076SAndreas Gohr if (!$mod->canAuthLogin() && $mod->canUse()) { 747*fca58076SAndreas Gohr $useableMods[$mod->getLang("name")] = $mod; 748*fca58076SAndreas Gohr } 749*fca58076SAndreas Gohr } 750*fca58076SAndreas Gohr $defaultMod = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor", 751*fca58076SAndreas Gohr "defaultmod") : null; 752*fca58076SAndreas Gohr $mod = array_key_exists($defaultMod, $useableMods) ? $useableMods[$defaultMod] : null; 753*fca58076SAndreas Gohr $this->_send_otp($mod); 754*fca58076SAndreas Gohr } 755*fca58076SAndreas Gohr // Generate the form to login. 756*fca58076SAndreas Gohr // If we are here, then only provide options to accept the OTP or to logout. 757*fca58076SAndreas Gohr global $lang; 758*fca58076SAndreas Gohr $form = new Doku_Form(array('id' => 'otp_setup')); 759*fca58076SAndreas Gohr $form->startFieldset($this->getLang('twofactor_otplogin')); 760*fca58076SAndreas Gohr $form->addElement(form_makeTextField('otpcode', '', $this->getLang('twofactor_otplogin'), '', 'block', 761*fca58076SAndreas Gohr array('size' => '50', 'autocomplete' => 'off'))); 762*fca58076SAndreas Gohr $form->addElement(form_makeButton('submit', '', $this->getLang('btn_login'))); 763*fca58076SAndreas Gohr $form->addElement(form_makeTag('br')); 764*fca58076SAndreas Gohr $form->addElement(form_makeCheckboxField('useall', '1', $this->getLang('twofactor_useallmods'), '', 'block')); 765*fca58076SAndreas Gohr $form->addElement(form_makeTag('br')); 766*fca58076SAndreas Gohr $form->addElement(form_makeButton('submit', '', $this->getLang('btn_resend'), array('name' => 'resend'))); 767*fca58076SAndreas Gohr $form->addElement(form_makeButton('submit', '', $this->getLang('btn_quit'), array('name' => 'otpquit'))); 768*fca58076SAndreas Gohr $form->endFieldset(); 769*fca58076SAndreas Gohr echo '<div class="centeralign">' . NL . $form->getForm() . '</div>' . NL; 770*fca58076SAndreas Gohr } 771*fca58076SAndreas Gohr 772*fca58076SAndreas Gohr /** 773*fca58076SAndreas Gohr * Sends a message using configured modules. 774*fca58076SAndreas Gohr * If $module is set to a specific instance, that instance will be used to 775*fca58076SAndreas Gohr * send the message. If not supplied or null, then all configured modules 776*fca58076SAndreas Gohr * will be used to send the message. $module can also be an array of 777*fca58076SAndreas Gohr * selected modules. 778*fca58076SAndreas Gohr * If $force is true, then will try to send the message even if the module 779*fca58076SAndreas Gohr * has not been validated. 780*fca58076SAndreas Gohr * @return array(array, mixed) - The first item in the array is an array 781*fca58076SAndreas Gohr * of all modules that the message was successfully sent by. The 782*fca58076SAndreas Gohr * second item is true if successfull to all attempted tramsmission 783*fca58076SAndreas Gohr * modules, false if all failed, and a number of how many successes 784*fca58076SAndreas Gohr * if only some modules failed. 785*fca58076SAndreas Gohr */ 786*fca58076SAndreas Gohr private function _send_message($subject, $message, $module = null, $force = false) 787*fca58076SAndreas Gohr { 788*fca58076SAndreas Gohr global $INPUT; 789*fca58076SAndreas Gohr $this->log("_send_message: start", self::LOGGING_DEBUG); 790*fca58076SAndreas Gohr if ($module === null) { 791*fca58076SAndreas Gohr $module = $this->otpMods; 792*fca58076SAndreas Gohr } 793*fca58076SAndreas Gohr if (!is_array($module)) { 794*fca58076SAndreas Gohr $module = array($module); 795*fca58076SAndreas Gohr } 796*fca58076SAndreas Gohr if (count($module) >= 1) { 797*fca58076SAndreas Gohr $modulekeys = array_keys($module); 798*fca58076SAndreas Gohr $modulekey = $modulekeys[0]; 799*fca58076SAndreas Gohr $modname = get_class($module[$modulekey]); 800*fca58076SAndreas Gohr } else { 801*fca58076SAndreas Gohr $modname = null; 802*fca58076SAndreas Gohr } 803*fca58076SAndreas Gohr // Attempt to deliver messages. 804*fca58076SAndreas Gohr $user = $INPUT->server->str('REMOTE_USER', '*unknown*'); 805*fca58076SAndreas Gohr $success = 0; 806*fca58076SAndreas Gohr $modname = array(); 807*fca58076SAndreas Gohr foreach ($module as $mod) { 808*fca58076SAndreas Gohr if ($mod->canTransmitMessage()) { 809*fca58076SAndreas Gohr $worked = $mod->transmitMessage($subject, $message, $force); 810*fca58076SAndreas Gohr if ($worked) { 811*fca58076SAndreas Gohr $success += 1; 812*fca58076SAndreas Gohr $modname[] = get_class($mod); 813*fca58076SAndreas Gohr } 814*fca58076SAndreas Gohr $this->log("Message " . ($worked ? '' : 'not ') . "sent to $user via " . get_class($mod), 815*fca58076SAndreas Gohr self::LOGGING_AUDITPLUS); 816*fca58076SAndreas Gohr } 817*fca58076SAndreas Gohr } 818*fca58076SAndreas Gohr return array($modname, $success == 0 ? false : ($success == count($module) ? true : $success)); 819*fca58076SAndreas Gohr } 820*fca58076SAndreas Gohr 821*fca58076SAndreas Gohr /** 822*fca58076SAndreas Gohr * Transmits a One-Time Password (OTP) using configured modules. 823*fca58076SAndreas Gohr * If $module is set to a specific instance, that instance will be used to 824*fca58076SAndreas Gohr * send the OTP. If not supplied or null, then all configured modules will 825*fca58076SAndreas Gohr * be used to send the OTP. $module can also be an array of selected 826*fca58076SAndreas Gohr * modules. 827*fca58076SAndreas Gohr * If $force is true, then will try to send the message even if the module 828*fca58076SAndreas Gohr * has not been validated. 829*fca58076SAndreas Gohr * @return mixed - true if successfull to all attempted tramsmission 830*fca58076SAndreas Gohr * modules, false if all failed, and a number of how many successes 831*fca58076SAndreas Gohr * if only some modules failed. 832*fca58076SAndreas Gohr */ 833*fca58076SAndreas Gohr private function _send_otp($module = null, $force = false) 834*fca58076SAndreas Gohr { 835*fca58076SAndreas Gohr $this->log("_send_otp: start", self::LOGGING_DEBUG); 836*fca58076SAndreas Gohr // Generate the OTP code. 837*fca58076SAndreas Gohr $characters = '0123456789'; 838*fca58076SAndreas Gohr $otp = ''; 839*fca58076SAndreas Gohr for ($index = 0; $index < $this->getConf('otplength'); ++$index) { 840*fca58076SAndreas Gohr $otp .= $characters[rand(0, strlen($characters) - 1)]; 841*fca58076SAndreas Gohr } 842*fca58076SAndreas Gohr // Create the subject. 843*fca58076SAndreas Gohr $subject = $this->getConf('otpsubject'); 844*fca58076SAndreas Gohr // Create the message. 845*fca58076SAndreas Gohr $message = str_replace('$otp', $otp, $this->getConf('otpcontent')); 846*fca58076SAndreas Gohr // Attempt to deliver the message. 847*fca58076SAndreas Gohr list($modname, $result) = $this->_send_message($subject, $message, $module, $force); 848*fca58076SAndreas Gohr // If partially successful, store the OTP code and the timestamp the OTP expires at. 849*fca58076SAndreas Gohr if ($result) { 850*fca58076SAndreas Gohr $otpData = array($otp, time() + $this->getConf('sentexpiry') * 60, $modname); 851*fca58076SAndreas Gohr if (!$this->attribute->set("twofactor", "otp", $otpData)) { 852*fca58076SAndreas Gohr msg("Unable to record OTP for later use.", -1); 853*fca58076SAndreas Gohr } 854*fca58076SAndreas Gohr } 855*fca58076SAndreas Gohr return $result; 856*fca58076SAndreas Gohr } 857*fca58076SAndreas Gohr 858*fca58076SAndreas Gohr /** 859*fca58076SAndreas Gohr * Returns the OTP code sent to the user, if it has not expired. 860*fca58076SAndreas Gohr * @return mixed - false if there is no unexpired OTP, otherwise 861*fca58076SAndreas Gohr * array of the OTP and the modules that successfully sent it. 862*fca58076SAndreas Gohr */ 863*fca58076SAndreas Gohr public function get_otp_code() 864*fca58076SAndreas Gohr { 865*fca58076SAndreas Gohr $this->log("get_otp_code: start", self::LOGGING_DEBUG); 866*fca58076SAndreas Gohr $otpQuery = $this->attribute->get("twofactor", "otp", $success); 867*fca58076SAndreas Gohr if (!$success) { 868*fca58076SAndreas Gohr return false; 869*fca58076SAndreas Gohr } 870*fca58076SAndreas Gohr list($otp, $expiry, $modname) = $otpQuery; 871*fca58076SAndreas Gohr if (time() > $expiry) { 872*fca58076SAndreas Gohr $this->attribute->del("twofactor", "otp"); 873*fca58076SAndreas Gohr return false; 874*fca58076SAndreas Gohr } 875*fca58076SAndreas Gohr return array($otp, $modname); 876*fca58076SAndreas Gohr } 877*fca58076SAndreas Gohr 878*fca58076SAndreas Gohr private function _setHelperVariables($user = null) 879*fca58076SAndreas Gohr { 880*fca58076SAndreas Gohr $this->log("_setHelperVariables: start", self::LOGGING_DEBUGPLUS); 881*fca58076SAndreas Gohr $tokenMods = array(); 882*fca58076SAndreas Gohr $otpMods = array(); 883*fca58076SAndreas Gohr $state = $this->attribute->get("twofactor", "state"); 884*fca58076SAndreas Gohr $optinout = $this->getConf("optinout"); 885*fca58076SAndreas Gohr $enabled = $optinout == 'mandatory' || ($state == '' ? $optinout == 'optin' : $state == 'in'); 886*fca58076SAndreas Gohr $this->log("_setHelperVariables: " . print_r(array($optinout, $state, $enabled), true), self::LOGGING_DEBUG); 887*fca58076SAndreas Gohr // Skip if not enabled for user 888*fca58076SAndreas Gohr if ($enabled) { 889*fca58076SAndreas Gohr // List all working token modules (GA, RSA, etc.). 890*fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 891*fca58076SAndreas Gohr if ($mod->canAuthLogin() && $mod->canUse($user)) { 892*fca58076SAndreas Gohr $this->log('Can use ' . get_class($mod) . ' for tokens', self::LOGGING_DEBUG); 893*fca58076SAndreas Gohr $tokenMods[$mod->getLang("name")] = $mod; 894*fca58076SAndreas Gohr } else { 895*fca58076SAndreas Gohr $this->log('Can NOT use ' . get_class($mod) . ' for tokens', self::LOGGING_DEBUG); 896*fca58076SAndreas Gohr } 897*fca58076SAndreas Gohr } 898*fca58076SAndreas Gohr // List all working OTP modules (SMS, Twilio, etc.). 899*fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 900*fca58076SAndreas Gohr if (!$mod->canAuthLogin() && $mod->canUse($user)) { 901*fca58076SAndreas Gohr $this->log('Can use ' . get_class($mod) . ' for otp', self::LOGGING_DEBUG); 902*fca58076SAndreas Gohr $otpMods[$mod->getLang("name")] = $mod; 903*fca58076SAndreas Gohr } else { 904*fca58076SAndreas Gohr $this->log('Can NOT use ' . get_class($mod) . ' for otp', self::LOGGING_DEBUG); 905*fca58076SAndreas Gohr } 906*fca58076SAndreas Gohr } 907*fca58076SAndreas Gohr } 908*fca58076SAndreas Gohr $this->tokenMods = $tokenMods; 909*fca58076SAndreas Gohr $this->otpMods = $otpMods; 910*fca58076SAndreas Gohr } 911*fca58076SAndreas Gohr 912*fca58076SAndreas Gohr const LOGGING_AUDIT = 1; // Audit records 2FA login and logout activity. 913*fca58076SAndreas Gohr const LOGGING_AUDITPLUS = 2; // Audit+ also records sending of notifications. 914*fca58076SAndreas Gohr const LOGGING_DEBUG = 3; // Debug provides detailed workflow data. 915*fca58076SAndreas Gohr const LOGGING_DEBUGPLUS = 4; // Debug+ also includes variables passed to and from functions. 916*fca58076SAndreas Gohr 917*fca58076SAndreas Gohr public function log($message, $level = 1) 918*fca58076SAndreas Gohr { 919*fca58076SAndreas Gohr // If the log level requested is below audit or greater than what is permitted in the configuration, then exit. 920*fca58076SAndreas Gohr if ($level < self::LOGGING_AUDIT || $level > $this->getConf('logging_level')) { 921*fca58076SAndreas Gohr return; 922*fca58076SAndreas Gohr } 923*fca58076SAndreas Gohr global $conf; 924*fca58076SAndreas Gohr // Always purge line containing "[pass]". 925*fca58076SAndreas Gohr $message = implode("\n", array_filter(explode("\n", $message), function ($x) { 926*fca58076SAndreas Gohr return !strstr($x, '[pass]'); 927*fca58076SAndreas Gohr })); 928*fca58076SAndreas Gohr // If DEBUGPLUS, then append the trace log. 929*fca58076SAndreas Gohr if ($level == self::LOGGING_DEBUGPLUS) { 930*fca58076SAndreas Gohr $e = new Exception(); 931*fca58076SAndreas Gohr $message .= "\n" . print_r(str_replace(DOKU_REL, '', $e->getTraceAsString()), true); 932*fca58076SAndreas Gohr } 933*fca58076SAndreas Gohr $logfile = $this->getConf('logging_path'); 934*fca58076SAndreas Gohr $logfile = substr($logfile, 0, 1) == '/' ? $logfile : DOKU_INC . $conf['savedir'] . '/' . $logfile; 935*fca58076SAndreas Gohr io_lock($logfile); 936*fca58076SAndreas Gohr #open for append logfile 937*fca58076SAndreas Gohr $handle = @fopen($logfile, 'at'); 938*fca58076SAndreas Gohr if ($handle) { 939*fca58076SAndreas Gohr $date = date(DATE_RFC2822); 940*fca58076SAndreas Gohr $IP = $_SERVER["REMOTE_ADDR"]; 941*fca58076SAndreas Gohr $id = session_id(); 942*fca58076SAndreas Gohr fwrite($handle, "$date,$id,$IP,$level,\"$message\"\n"); 943*fca58076SAndreas Gohr fclose($handle); 944*fca58076SAndreas Gohr } 945*fca58076SAndreas Gohr #write "date level message" 946*fca58076SAndreas Gohr io_unlock($logfile); 947*fca58076SAndreas Gohr } 948*fca58076SAndreas Gohr} 949