register_hook( 'ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleActionPreProcess', null, Manager::EVENT_PRIORITY ); // display login form $controller->register_hook( 'TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handleLoginDisplay' ); // disable user in all non-main screens (media, detail, ajax, ...) $controller->register_hook( 'DOKUWIKI_INIT_DONE', 'BEFORE', $this, 'handleInitDone' ); } /** * Decide if any 2fa handling needs to be done for the current user * * @param Doku_Event $event */ public function handleActionPreProcess(Doku_Event $event) { if ($event->data === 'resendpwd') { // this is completely handled in resendpwd.php return; } $manager = Manager::getInstance(); if (!$manager->isReady()) return; global $INPUT; // already in a 2fa login? if ($event->data === 'twofactor_login') { if ($this->verify( $INPUT->str('2fa_code'), $INPUT->str('2fa_provider'), $this->isSticky() )) { $event->data = 'show'; return; } else { // show form $event->preventDefault(); return; } } // clear cookie on logout if ($event->data === 'logout') { $this->deAuth(); return; } // authed already, continue if ($this->isAuthed()) { return; } if (count($manager->getUserProviders())) { // user has already 2fa set up - they need to authenticate before anything else $event->data = 'twofactor_login'; $event->preventDefault(); $event->stopPropagation(); return; } if ($manager->isRequired()) { // 2fa is required - they need to set it up now // this will be handled by action/profile.php $event->data = 'twofactor_profile'; } // all good. proceed } /** * Show a 2fa login screen * * @param Doku_Event $event */ public function handleLoginDisplay(Doku_Event $event) { if ($event->data !== 'twofactor_login') return; $manager = Manager::getInstance(); if (!$manager->isReady()) return; $event->preventDefault(); $event->stopPropagation(); global $INPUT; $providerID = $INPUT->str('2fa_provider'); echo '
'; echo inlineSVG(__DIR__ . '/../admin.svg'); echo $this->locale_xhtml('login'); echo $manager->getCodeForm($providerID)->toHTML(); echo '
'; } /** * Remove user info from non-main entry points while we wait for 2fa * * @param Doku_Event $event */ public function handleInitDone(Doku_Event $event) { global $INPUT; if (!(Manager::getInstance())->isReady()) return; if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return; if ($this->isAuthed()) return; if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return; // temporarily remove user info from environment $INPUT->server->remove('REMOTE_USER'); unset($_SESSION[DOKU_COOKIE]['auth']); unset($GLOBALS['USERINFO']); } /** * Has the user already authenticated with the second factor? * @return bool */ protected function isAuthed() { // if we trust the IP, we don't need 2fa and consider the user authed automatically if ( $this->getConf('trustedIPs') && preg_match('/' . $this->getConf('trustedIPs') . '/', clientIP(true)) ) { return true; } if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); if (!is_array($data)) return false; list($providerID, $hash,) = $data; try { $provider = (Manager::getInstance())->getUserProvider($providerID); if ($this->cookieHash($provider) !== $hash) return false; return true; } catch (Exception $ignored) { return false; } } /** * Get sticky value from standard cookie * * @return bool */ protected function isSticky() { if (!isset($_COOKIE[DOKU_COOKIE])) { return false; } list(, $sticky,) = explode('|', $_COOKIE[DOKU_COOKIE], 3); return (bool)$sticky; } /** * Deletes the cookie * * @return void */ protected function deAuth() { global $conf; $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; $time = time() - 60 * 60 * 24 * 365; // one year in the past setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); } /** * Verify a given code * * @return bool * @throws Exception */ protected function verify($code, $providerID, $sticky) { global $conf; $manager = Manager::getInstance(); if (!$manager->verifyCode($code, $providerID)) return false; $provider = (Manager::getInstance())->getUserProvider($providerID); // store cookie $hash = $this->cookieHash($provider); $data = base64_encode(serialize([$providerID, $hash, time()])); $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; $time = $sticky ? (time() + 60 * 60 * 24 * 30 * 3) : 0; //three months on sticky login setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); return true; } /** * Create a hash that validates the cookie * * @param Provider $provider * @return string */ protected function cookieHash($provider) { return sha1(join("\n", [ $provider->getProviderID(), (Manager::getInstance())->getUser(), $provider->getSecret(), $this->getConf("useinternaluid") ? auth_browseruid() : $_SERVER['HTTP_USER_AGENT'], auth_cookiesalt(false, true), ])); } }