1fca58076SAndreas Gohr<?php 28b7620a8SAndreas Gohr 38b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager; 4a01d09a8SAndreas Gohruse dokuwiki\plugin\twofactor\OtpField; 56c996db8SAndreas Gohruse dokuwiki\plugin\twofactor\Provider; 68b7620a8SAndreas Gohr 730625b49SAndreas Gohr/** 830625b49SAndreas Gohr * DokuWiki Plugin twofactor (Action Component) 930625b49SAndreas Gohr * 1030625b49SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 1130625b49SAndreas Gohr */ 12fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin 13fca58076SAndreas Gohr{ 14848a9be0SAndreas Gohr const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE; 15848a9be0SAndreas Gohr 16fca58076SAndreas Gohr /** 17fca58076SAndreas Gohr * Registers the event handlers. 18fca58076SAndreas Gohr */ 19fca58076SAndreas Gohr public function register(Doku_Event_Handler $controller) 20fca58076SAndreas Gohr { 21a386a536SAndreas Gohr // check 2fa requirements and either move to profile or login handling 22a386a536SAndreas Gohr $controller->register_hook( 23a386a536SAndreas Gohr 'ACTION_ACT_PREPROCESS', 24a386a536SAndreas Gohr 'BEFORE', 25a386a536SAndreas Gohr $this, 26a386a536SAndreas Gohr 'handleActionPreProcess', 27a386a536SAndreas Gohr null, 285f8f561aSAndreas Gohr Manager::EVENT_PRIORITY 29a386a536SAndreas Gohr ); 30fca58076SAndreas Gohr 31a386a536SAndreas Gohr // display login form 32a386a536SAndreas Gohr $controller->register_hook( 33a386a536SAndreas Gohr 'TPL_ACT_UNKNOWN', 34a386a536SAndreas Gohr 'BEFORE', 35a386a536SAndreas Gohr $this, 36a386a536SAndreas Gohr 'handleLoginDisplay' 37a386a536SAndreas Gohr ); 38a386a536SAndreas Gohr 39210d81e3SAndreas Gohr // disable user in all non-main screens (media, detail, ajax, ...) 40210d81e3SAndreas Gohr $controller->register_hook( 41210d81e3SAndreas Gohr 'DOKUWIKI_INIT_DONE', 42210d81e3SAndreas Gohr 'BEFORE', 43210d81e3SAndreas Gohr $this, 44210d81e3SAndreas Gohr 'handleInitDone' 45210d81e3SAndreas Gohr ); 46fca58076SAndreas Gohr } 47fca58076SAndreas Gohr 48fca58076SAndreas Gohr /** 49a386a536SAndreas Gohr * Decide if any 2fa handling needs to be done for the current user 50a386a536SAndreas Gohr * 51a386a536SAndreas Gohr * @param Doku_Event $event 52a386a536SAndreas Gohr */ 53a386a536SAndreas Gohr public function handleActionPreProcess(Doku_Event $event) 54a386a536SAndreas Gohr { 555f8f561aSAndreas Gohr $manager = Manager::getInstance(); 565f8f561aSAndreas Gohr if (!$manager->isReady()) return; 57a386a536SAndreas Gohr 58a386a536SAndreas Gohr global $INPUT; 59a386a536SAndreas Gohr 60a386a536SAndreas Gohr // already in a 2fa login? 61a386a536SAndreas Gohr if ($event->data === 'twofactor_login') { 62848a9be0SAndreas Gohr if ($this->verify( 63848a9be0SAndreas Gohr $INPUT->str('2fa_code'), 64848a9be0SAndreas Gohr $INPUT->str('2fa_provider'), 6503bae0e0SAndreas Gohr $this->isSticky() 66848a9be0SAndreas Gohr )) { 67a386a536SAndreas Gohr $event->data = 'show'; 68848a9be0SAndreas Gohr return; 69a386a536SAndreas Gohr } else { 70a386a536SAndreas Gohr // show form 71a386a536SAndreas Gohr $event->preventDefault(); 72a386a536SAndreas Gohr return; 73a386a536SAndreas Gohr } 74a386a536SAndreas Gohr } 75a386a536SAndreas Gohr 76857c5abcSAndreas Gohr // clear cookie on logout 77857c5abcSAndreas Gohr if ($event->data === 'logout') { 78857c5abcSAndreas Gohr $this->deAuth(); 79857c5abcSAndreas Gohr return; 80857c5abcSAndreas Gohr } 81857c5abcSAndreas Gohr 82a386a536SAndreas Gohr // authed already, continue 83a386a536SAndreas Gohr if ($this->isAuthed()) { 84a386a536SAndreas Gohr return; 85a386a536SAndreas Gohr } 86a386a536SAndreas Gohr 875f8f561aSAndreas Gohr if (count($manager->getUserProviders())) { 88a386a536SAndreas Gohr // user has already 2fa set up - they need to authenticate before anything else 89a386a536SAndreas Gohr $event->data = 'twofactor_login'; 90a386a536SAndreas Gohr $event->preventDefault(); 91a386a536SAndreas Gohr $event->stopPropagation(); 92a386a536SAndreas Gohr return; 93a386a536SAndreas Gohr } 94a386a536SAndreas Gohr 955f8f561aSAndreas Gohr if ($manager->isRequired()) { 96a386a536SAndreas Gohr // 2fa is required - they need to set it up now 97a386a536SAndreas Gohr // this will be handled by action/profile.php 98a386a536SAndreas Gohr $event->data = 'twofactor_profile'; 99a386a536SAndreas Gohr } 100a386a536SAndreas Gohr 101a386a536SAndreas Gohr // all good. proceed 102a386a536SAndreas Gohr } 103a386a536SAndreas Gohr 104a386a536SAndreas Gohr /** 105a386a536SAndreas Gohr * Show a 2fa login screen 106a386a536SAndreas Gohr * 107a386a536SAndreas Gohr * @param Doku_Event $event 108a386a536SAndreas Gohr */ 109a386a536SAndreas Gohr public function handleLoginDisplay(Doku_Event $event) 110a386a536SAndreas Gohr { 111a386a536SAndreas Gohr if ($event->data !== 'twofactor_login') return; 1125f8f561aSAndreas Gohr $manager = Manager::getInstance(); 1135f8f561aSAndreas Gohr if (!$manager->isReady()) return; 1145f8f561aSAndreas Gohr 115a386a536SAndreas Gohr $event->preventDefault(); 116a386a536SAndreas Gohr $event->stopPropagation(); 117a386a536SAndreas Gohr 118a386a536SAndreas Gohr global $INPUT; 119c9e42a8dSAndreas Gohr global $ID; 120c9e42a8dSAndreas Gohr 121a386a536SAndreas Gohr $providerID = $INPUT->str('2fa_provider'); 1225f8f561aSAndreas Gohr $providers = $manager->getUserProviders(); 1235f8f561aSAndreas Gohr $provider = $providers[$providerID] ?? $manager->getUserDefaultProvider(); 124b6119621SAndreas Gohr // remove current provider from list 125b6119621SAndreas Gohr unset($providers[$provider->getProviderID()]); 126a386a536SAndreas Gohr 1270407d282SAndreas Gohr echo '<div class="plugin_twofactor_login">'; 1280407d282SAndreas Gohr echo inlineSVG(__DIR__ . '/../admin.svg'); 129857c5abcSAndreas Gohr echo $this->locale_xhtml('login'); 130848a9be0SAndreas Gohr $form = new dokuwiki\Form\Form(['method' => 'POST']); 131848a9be0SAndreas Gohr $form->setHiddenField('do', 'twofactor_login'); 132a386a536SAndreas Gohr $form->setHiddenField('2fa_provider', $provider->getProviderID()); 133a386a536SAndreas Gohr $form->addFieldsetOpen($provider->getLabel()); 134a386a536SAndreas Gohr try { 135a386a536SAndreas Gohr $code = $provider->generateCode(); 136a386a536SAndreas Gohr $info = $provider->transmitMessage($code); 137a386a536SAndreas Gohr $form->addHTML('<p>' . hsc($info) . '</p>'); 138a01d09a8SAndreas Gohr $form->addElement(new OtpField('2fa_code')); 1390407d282SAndreas Gohr $form->addTagOpen('div')->addClass('buttons'); 140a01d09a8SAndreas Gohr $form->addButton('2fa', $this->getLang('btn_confirm'))->attr('type', 'submit'); 1410407d282SAndreas Gohr $form->addTagClose('div'); 1425f8f561aSAndreas Gohr } catch (Exception $e) { 143a386a536SAndreas Gohr msg(hsc($e->getMessage()), -1); // FIXME better handling 144a386a536SAndreas Gohr } 145a386a536SAndreas Gohr $form->addFieldsetClose(); 146a386a536SAndreas Gohr 147a386a536SAndreas Gohr if (count($providers)) { 14878279978SAndreas Gohr $form->addFieldsetOpen('Alternative methods')->addClass('list'); 14978279978SAndreas Gohr $form->addTagOpen('ul'); 150a386a536SAndreas Gohr foreach ($providers as $prov) { 151c9e42a8dSAndreas Gohr $url = wl($ID, [ 152c9e42a8dSAndreas Gohr 'do' => 'twofactor_login', 153c9e42a8dSAndreas Gohr '2fa_provider' => $prov->getProviderID(), 154c9e42a8dSAndreas Gohr ]); 15578279978SAndreas Gohr $form->addHTML( 15678279978SAndreas Gohr '<li><div class="li"><a href="' . $url . '">' . hsc($prov->getLabel()) . '</a></div></li>' 15778279978SAndreas Gohr ); 158a386a536SAndreas Gohr } 15978279978SAndreas Gohr 16078279978SAndreas Gohr $form->addTagClose('ul'); 161a386a536SAndreas Gohr $form->addFieldsetClose(); 162a386a536SAndreas Gohr } 163a386a536SAndreas Gohr 164a386a536SAndreas Gohr echo $form->toHTML(); 1650407d282SAndreas Gohr echo '</div>'; 166a386a536SAndreas Gohr } 167a386a536SAndreas Gohr 168a386a536SAndreas Gohr /** 169210d81e3SAndreas Gohr * Remove user info from non-main entry points while we wait for 2fa 170210d81e3SAndreas Gohr * 171210d81e3SAndreas Gohr * @param Doku_Event $event 172210d81e3SAndreas Gohr */ 173210d81e3SAndreas Gohr public function handleInitDone(Doku_Event $event) 174210d81e3SAndreas Gohr { 175210d81e3SAndreas Gohr global $INPUT; 176210d81e3SAndreas Gohr 177210d81e3SAndreas Gohr if (!(Manager::getInstance())->isReady()) return; 178210d81e3SAndreas Gohr if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return; 179210d81e3SAndreas Gohr if ($this->isAuthed()) return; 180210d81e3SAndreas Gohr 1810d5f8055SAnna Dabrowska if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return; 1820d5f8055SAnna Dabrowska 183210d81e3SAndreas Gohr // temporarily remove user info from environment 184210d81e3SAndreas Gohr $INPUT->server->remove('REMOTE_USER'); 185210d81e3SAndreas Gohr unset($_SESSION[DOKU_COOKIE]['auth']); 186210d81e3SAndreas Gohr unset($GLOBALS['USERINFO']); 187210d81e3SAndreas Gohr } 188210d81e3SAndreas Gohr 189210d81e3SAndreas Gohr /** 190a386a536SAndreas Gohr * Has the user already authenticated with the second factor? 191a386a536SAndreas Gohr * @return bool 192a386a536SAndreas Gohr */ 193a386a536SAndreas Gohr protected function isAuthed() 194a386a536SAndreas Gohr { 195848a9be0SAndreas Gohr if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 196848a9be0SAndreas Gohr $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 197848a9be0SAndreas Gohr if (!is_array($data)) return false; 1986c996db8SAndreas Gohr list($providerID, $hash,) = $data; 199848a9be0SAndreas Gohr 200848a9be0SAndreas Gohr try { 2015f8f561aSAndreas Gohr $provider = (Manager::getInstance())->getUserProvider($providerID); 2026c996db8SAndreas Gohr if ($this->cookieHash($provider) !== $hash) return false; 203848a9be0SAndreas Gohr return true; 2045f8f561aSAndreas Gohr } catch (Exception $ignored) { 205a386a536SAndreas Gohr return false; 206a386a536SAndreas Gohr } 207848a9be0SAndreas Gohr } 208a386a536SAndreas Gohr 209a386a536SAndreas Gohr /** 21003bae0e0SAndreas Gohr * Get sticky value from standard cookie 21103bae0e0SAndreas Gohr * 21203bae0e0SAndreas Gohr * @return bool 21303bae0e0SAndreas Gohr */ 21403bae0e0SAndreas Gohr protected function isSticky() 21503bae0e0SAndreas Gohr { 21603bae0e0SAndreas Gohr if (!isset($_COOKIE[DOKU_COOKIE])) { 21703bae0e0SAndreas Gohr return false; 21803bae0e0SAndreas Gohr } 21903bae0e0SAndreas Gohr list(, $sticky,) = explode('|', $_COOKIE[DOKU_COOKIE], 3); 22003bae0e0SAndreas Gohr return (bool)$sticky; 22103bae0e0SAndreas Gohr } 22203bae0e0SAndreas Gohr 22303bae0e0SAndreas Gohr /** 224857c5abcSAndreas Gohr * Deletes the cookie 225857c5abcSAndreas Gohr * 226857c5abcSAndreas Gohr * @return void 227857c5abcSAndreas Gohr */ 228857c5abcSAndreas Gohr protected function deAuth() 229857c5abcSAndreas Gohr { 230857c5abcSAndreas Gohr global $conf; 231857c5abcSAndreas Gohr 232857c5abcSAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 233857c5abcSAndreas Gohr $time = time() - 60 * 60 * 24 * 365; // one year in the past 234857c5abcSAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 235857c5abcSAndreas Gohr } 236857c5abcSAndreas Gohr 237857c5abcSAndreas Gohr /** 238a386a536SAndreas Gohr * Verify a given code 239a386a536SAndreas Gohr * 240a386a536SAndreas Gohr * @return bool 241a386a536SAndreas Gohr * @throws Exception 242a386a536SAndreas Gohr */ 243848a9be0SAndreas Gohr protected function verify($code, $providerID, $sticky) 244a386a536SAndreas Gohr { 245848a9be0SAndreas Gohr global $conf; 246848a9be0SAndreas Gohr 247a386a536SAndreas Gohr if (!$code) return false; 248a386a536SAndreas Gohr if (!$providerID) return false; 2495f8f561aSAndreas Gohr $provider = (Manager::getInstance())->getUserProvider($providerID); 250a386a536SAndreas Gohr $ok = $provider->checkCode($code); 25116ed3964SAndreas Gohr if (!$ok) return false; 252a386a536SAndreas Gohr 253848a9be0SAndreas Gohr // store cookie 2546c996db8SAndreas Gohr $hash = $this->cookieHash($provider); 2556c996db8SAndreas Gohr $data = base64_encode(serialize([$providerID, $hash, time()])); 256848a9be0SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 25703bae0e0SAndreas Gohr $time = $sticky ? (time() + 60 * 60 * 24 * 30 * 3) : 0; //three months on sticky login 258848a9be0SAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 259a386a536SAndreas Gohr 260a386a536SAndreas Gohr return true; 261a386a536SAndreas Gohr } 2626c996db8SAndreas Gohr 2636c996db8SAndreas Gohr /** 2646c996db8SAndreas Gohr * Create a hash that validates the cookie 2656c996db8SAndreas Gohr * 2666c996db8SAndreas Gohr * @param Provider $provider 2676c996db8SAndreas Gohr * @return string 2686c996db8SAndreas Gohr */ 2696c996db8SAndreas Gohr protected function cookieHash($provider) 2706c996db8SAndreas Gohr { 2716c996db8SAndreas Gohr return sha1(join("\n", [ 2726c996db8SAndreas Gohr $provider->getProviderID(), 2735f8f561aSAndreas Gohr (Manager::getInstance())->getUser(), 2746c996db8SAndreas Gohr $provider->getSecret(), 275*09c2ba1aSalexdraconian $this->getConf("useinternaluid") ? auth_browseruid() : $_SERVER['HTTP_USER_AGENT'], 2766c996db8SAndreas Gohr auth_cookiesalt(false, true), 2776c996db8SAndreas Gohr ])); 2786c996db8SAndreas Gohr } 279fca58076SAndreas Gohr} 280