1fca58076SAndreas Gohr<?php 28b7620a8SAndreas Gohr 38b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager; 46c996db8SAndreas Gohruse dokuwiki\plugin\twofactor\Provider; 58b7620a8SAndreas Gohr 630625b49SAndreas Gohr/** 730625b49SAndreas Gohr * DokuWiki Plugin twofactor (Action Component) 830625b49SAndreas Gohr * 930625b49SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 1030625b49SAndreas Gohr */ 11fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin 12fca58076SAndreas Gohr{ 13848a9be0SAndreas Gohr const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE; 14848a9be0SAndreas Gohr 15fca58076SAndreas Gohr /** 16fca58076SAndreas Gohr * Registers the event handlers. 17fca58076SAndreas Gohr */ 18fca58076SAndreas Gohr public function register(Doku_Event_Handler $controller) 19fca58076SAndreas Gohr { 20a386a536SAndreas Gohr // check 2fa requirements and either move to profile or login handling 21a386a536SAndreas Gohr $controller->register_hook( 22a386a536SAndreas Gohr 'ACTION_ACT_PREPROCESS', 23a386a536SAndreas Gohr 'BEFORE', 24a386a536SAndreas Gohr $this, 25a386a536SAndreas Gohr 'handleActionPreProcess', 26a386a536SAndreas Gohr null, 275f8f561aSAndreas Gohr Manager::EVENT_PRIORITY 28a386a536SAndreas Gohr ); 29fca58076SAndreas Gohr 30a386a536SAndreas Gohr // display login form 31a386a536SAndreas Gohr $controller->register_hook( 32a386a536SAndreas Gohr 'TPL_ACT_UNKNOWN', 33a386a536SAndreas Gohr 'BEFORE', 34a386a536SAndreas Gohr $this, 35a386a536SAndreas Gohr 'handleLoginDisplay' 36a386a536SAndreas Gohr ); 37a386a536SAndreas Gohr 38210d81e3SAndreas Gohr // disable user in all non-main screens (media, detail, ajax, ...) 39210d81e3SAndreas Gohr $controller->register_hook( 40210d81e3SAndreas Gohr 'DOKUWIKI_INIT_DONE', 41210d81e3SAndreas Gohr 'BEFORE', 42210d81e3SAndreas Gohr $this, 43210d81e3SAndreas Gohr 'handleInitDone' 44210d81e3SAndreas Gohr ); 45fca58076SAndreas Gohr } 46fca58076SAndreas Gohr 47fca58076SAndreas Gohr /** 48a386a536SAndreas Gohr * Decide if any 2fa handling needs to be done for the current user 49a386a536SAndreas Gohr * 50a386a536SAndreas Gohr * @param Doku_Event $event 51a386a536SAndreas Gohr */ 52a386a536SAndreas Gohr public function handleActionPreProcess(Doku_Event $event) 53a386a536SAndreas Gohr { 54c8525a21SAndreas Gohr if ($event->data === 'resendpwd') { 55c8525a21SAndreas Gohr // this is completely handled in resendpwd.php 56c8525a21SAndreas Gohr return; 57c8525a21SAndreas Gohr } 58c8525a21SAndreas Gohr 595f8f561aSAndreas Gohr $manager = Manager::getInstance(); 605f8f561aSAndreas Gohr if (!$manager->isReady()) return; 61a386a536SAndreas Gohr 62a386a536SAndreas Gohr global $INPUT; 63a386a536SAndreas Gohr 64a386a536SAndreas Gohr // already in a 2fa login? 65a386a536SAndreas Gohr if ($event->data === 'twofactor_login') { 66848a9be0SAndreas Gohr if ($this->verify( 67848a9be0SAndreas Gohr $INPUT->str('2fa_code'), 68848a9be0SAndreas Gohr $INPUT->str('2fa_provider'), 6903bae0e0SAndreas Gohr $this->isSticky() 70848a9be0SAndreas Gohr )) { 71a386a536SAndreas Gohr $event->data = 'show'; 72848a9be0SAndreas Gohr return; 73a386a536SAndreas Gohr } else { 74a386a536SAndreas Gohr // show form 75a386a536SAndreas Gohr $event->preventDefault(); 76a386a536SAndreas Gohr return; 77a386a536SAndreas Gohr } 78a386a536SAndreas Gohr } 79a386a536SAndreas Gohr 80857c5abcSAndreas Gohr // clear cookie on logout 81857c5abcSAndreas Gohr if ($event->data === 'logout') { 82857c5abcSAndreas Gohr $this->deAuth(); 83857c5abcSAndreas Gohr return; 84857c5abcSAndreas Gohr } 85857c5abcSAndreas Gohr 86a386a536SAndreas Gohr // authed already, continue 87a386a536SAndreas Gohr if ($this->isAuthed()) { 88a386a536SAndreas Gohr return; 89a386a536SAndreas Gohr } 90a386a536SAndreas Gohr 915f8f561aSAndreas Gohr if (count($manager->getUserProviders())) { 92a386a536SAndreas Gohr // user has already 2fa set up - they need to authenticate before anything else 93a386a536SAndreas Gohr $event->data = 'twofactor_login'; 94a386a536SAndreas Gohr $event->preventDefault(); 95a386a536SAndreas Gohr $event->stopPropagation(); 96a386a536SAndreas Gohr return; 97a386a536SAndreas Gohr } 98a386a536SAndreas Gohr 995f8f561aSAndreas Gohr if ($manager->isRequired()) { 100a386a536SAndreas Gohr // 2fa is required - they need to set it up now 101a386a536SAndreas Gohr // this will be handled by action/profile.php 102a386a536SAndreas Gohr $event->data = 'twofactor_profile'; 103a386a536SAndreas Gohr } 104a386a536SAndreas Gohr 105a386a536SAndreas Gohr // all good. proceed 106a386a536SAndreas Gohr } 107a386a536SAndreas Gohr 108a386a536SAndreas Gohr /** 109a386a536SAndreas Gohr * Show a 2fa login screen 110a386a536SAndreas Gohr * 111a386a536SAndreas Gohr * @param Doku_Event $event 112a386a536SAndreas Gohr */ 113a386a536SAndreas Gohr public function handleLoginDisplay(Doku_Event $event) 114a386a536SAndreas Gohr { 115a386a536SAndreas Gohr if ($event->data !== 'twofactor_login') return; 1165f8f561aSAndreas Gohr $manager = Manager::getInstance(); 1175f8f561aSAndreas Gohr if (!$manager->isReady()) return; 1185f8f561aSAndreas Gohr 119a386a536SAndreas Gohr $event->preventDefault(); 120a386a536SAndreas Gohr $event->stopPropagation(); 121a386a536SAndreas Gohr 122a386a536SAndreas Gohr global $INPUT; 123a386a536SAndreas Gohr $providerID = $INPUT->str('2fa_provider'); 124a386a536SAndreas Gohr 1250407d282SAndreas Gohr echo '<div class="plugin_twofactor_login">'; 1260407d282SAndreas Gohr echo inlineSVG(__DIR__ . '/../admin.svg'); 127857c5abcSAndreas Gohr echo $this->locale_xhtml('login'); 128c8525a21SAndreas Gohr echo $manager->getCodeForm($providerID)->toHTML(); 1290407d282SAndreas Gohr echo '</div>'; 130a386a536SAndreas Gohr } 131a386a536SAndreas Gohr 132a386a536SAndreas Gohr /** 133210d81e3SAndreas Gohr * Remove user info from non-main entry points while we wait for 2fa 134210d81e3SAndreas Gohr * 135210d81e3SAndreas Gohr * @param Doku_Event $event 136210d81e3SAndreas Gohr */ 137210d81e3SAndreas Gohr public function handleInitDone(Doku_Event $event) 138210d81e3SAndreas Gohr { 139210d81e3SAndreas Gohr global $INPUT; 140210d81e3SAndreas Gohr 141210d81e3SAndreas Gohr if (!(Manager::getInstance())->isReady()) return; 142210d81e3SAndreas Gohr if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return; 143210d81e3SAndreas Gohr if ($this->isAuthed()) return; 144210d81e3SAndreas Gohr 1450d5f8055SAnna Dabrowska if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return; 1460d5f8055SAnna Dabrowska 147210d81e3SAndreas Gohr // temporarily remove user info from environment 148210d81e3SAndreas Gohr $INPUT->server->remove('REMOTE_USER'); 149210d81e3SAndreas Gohr unset($_SESSION[DOKU_COOKIE]['auth']); 150210d81e3SAndreas Gohr unset($GLOBALS['USERINFO']); 151210d81e3SAndreas Gohr } 152210d81e3SAndreas Gohr 153210d81e3SAndreas Gohr /** 154a386a536SAndreas Gohr * Has the user already authenticated with the second factor? 155a386a536SAndreas Gohr * @return bool 156a386a536SAndreas Gohr */ 157a386a536SAndreas Gohr protected function isAuthed() 158a386a536SAndreas Gohr { 159*95ed3afaSAndreas Gohr // if we trust the IP, we don't need 2fa and consider the user authed automatically 160*95ed3afaSAndreas Gohr if ( 161*95ed3afaSAndreas Gohr $this->getConf('trustedIPs') && 162*95ed3afaSAndreas Gohr preg_match('/' . $this->getConf('trustedIPs') . '/', clientIP(true)) 163*95ed3afaSAndreas Gohr ) { 164*95ed3afaSAndreas Gohr return true; 165*95ed3afaSAndreas Gohr } 166*95ed3afaSAndreas Gohr 167848a9be0SAndreas Gohr if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 168848a9be0SAndreas Gohr $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 169848a9be0SAndreas Gohr if (!is_array($data)) return false; 1706c996db8SAndreas Gohr list($providerID, $hash,) = $data; 171848a9be0SAndreas Gohr 172848a9be0SAndreas Gohr try { 1735f8f561aSAndreas Gohr $provider = (Manager::getInstance())->getUserProvider($providerID); 1746c996db8SAndreas Gohr if ($this->cookieHash($provider) !== $hash) return false; 175848a9be0SAndreas Gohr return true; 1765f8f561aSAndreas Gohr } catch (Exception $ignored) { 177a386a536SAndreas Gohr return false; 178a386a536SAndreas Gohr } 179848a9be0SAndreas Gohr } 180a386a536SAndreas Gohr 181a386a536SAndreas Gohr /** 18203bae0e0SAndreas Gohr * Get sticky value from standard cookie 18303bae0e0SAndreas Gohr * 18403bae0e0SAndreas Gohr * @return bool 18503bae0e0SAndreas Gohr */ 18603bae0e0SAndreas Gohr protected function isSticky() 18703bae0e0SAndreas Gohr { 18803bae0e0SAndreas Gohr if (!isset($_COOKIE[DOKU_COOKIE])) { 18903bae0e0SAndreas Gohr return false; 19003bae0e0SAndreas Gohr } 19103bae0e0SAndreas Gohr list(, $sticky,) = explode('|', $_COOKIE[DOKU_COOKIE], 3); 19203bae0e0SAndreas Gohr return (bool)$sticky; 19303bae0e0SAndreas Gohr } 19403bae0e0SAndreas Gohr 19503bae0e0SAndreas Gohr /** 196857c5abcSAndreas Gohr * Deletes the cookie 197857c5abcSAndreas Gohr * 198857c5abcSAndreas Gohr * @return void 199857c5abcSAndreas Gohr */ 200857c5abcSAndreas Gohr protected function deAuth() 201857c5abcSAndreas Gohr { 202857c5abcSAndreas Gohr global $conf; 203857c5abcSAndreas Gohr 204857c5abcSAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 205857c5abcSAndreas Gohr $time = time() - 60 * 60 * 24 * 365; // one year in the past 206857c5abcSAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 207857c5abcSAndreas Gohr } 208857c5abcSAndreas Gohr 209857c5abcSAndreas Gohr /** 210a386a536SAndreas Gohr * Verify a given code 211a386a536SAndreas Gohr * 212a386a536SAndreas Gohr * @return bool 213a386a536SAndreas Gohr * @throws Exception 214a386a536SAndreas Gohr */ 215848a9be0SAndreas Gohr protected function verify($code, $providerID, $sticky) 216a386a536SAndreas Gohr { 217848a9be0SAndreas Gohr global $conf; 218848a9be0SAndreas Gohr 219c8525a21SAndreas Gohr $manager = Manager::getInstance(); 220c8525a21SAndreas Gohr if (!$manager->verifyCode($code, $providerID)) return false; 221c8525a21SAndreas Gohr 2225f8f561aSAndreas Gohr $provider = (Manager::getInstance())->getUserProvider($providerID); 223a386a536SAndreas Gohr 224848a9be0SAndreas Gohr // store cookie 2256c996db8SAndreas Gohr $hash = $this->cookieHash($provider); 2266c996db8SAndreas Gohr $data = base64_encode(serialize([$providerID, $hash, time()])); 227848a9be0SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 22803bae0e0SAndreas Gohr $time = $sticky ? (time() + 60 * 60 * 24 * 30 * 3) : 0; //three months on sticky login 229848a9be0SAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 230a386a536SAndreas Gohr 231a386a536SAndreas Gohr return true; 232a386a536SAndreas Gohr } 2336c996db8SAndreas Gohr 2346c996db8SAndreas Gohr /** 2356c996db8SAndreas Gohr * Create a hash that validates the cookie 2366c996db8SAndreas Gohr * 2376c996db8SAndreas Gohr * @param Provider $provider 2386c996db8SAndreas Gohr * @return string 2396c996db8SAndreas Gohr */ 2406c996db8SAndreas Gohr protected function cookieHash($provider) 2416c996db8SAndreas Gohr { 2426c996db8SAndreas Gohr return sha1(join("\n", [ 2436c996db8SAndreas Gohr $provider->getProviderID(), 2445f8f561aSAndreas Gohr (Manager::getInstance())->getUser(), 2456c996db8SAndreas Gohr $provider->getSecret(), 24609c2ba1aSalexdraconian $this->getConf("useinternaluid") ? auth_browseruid() : $_SERVER['HTTP_USER_AGENT'], 2476c996db8SAndreas Gohr auth_cookiesalt(false, true), 2486c996db8SAndreas Gohr ])); 2496c996db8SAndreas Gohr } 250fca58076SAndreas Gohr} 251