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 38a386a536SAndreas Gohr // FIXME disable user in all non-main screens (media, detail, ajax, ...) 39fca58076SAndreas Gohr } 40fca58076SAndreas Gohr 41fca58076SAndreas Gohr /** 42a386a536SAndreas Gohr * Decide if any 2fa handling needs to be done for the current user 43a386a536SAndreas Gohr * 44a386a536SAndreas Gohr * @param Doku_Event $event 45a386a536SAndreas Gohr */ 46a386a536SAndreas Gohr public function handleActionPreProcess(Doku_Event $event) 47a386a536SAndreas Gohr { 485f8f561aSAndreas Gohr $manager = Manager::getInstance(); 495f8f561aSAndreas Gohr if (!$manager->isReady()) return; 50a386a536SAndreas Gohr 51a386a536SAndreas Gohr global $INPUT; 52a386a536SAndreas Gohr 53a386a536SAndreas Gohr // already in a 2fa login? 54a386a536SAndreas Gohr if ($event->data === 'twofactor_login') { 55848a9be0SAndreas Gohr if ($this->verify( 56848a9be0SAndreas Gohr $INPUT->str('2fa_code'), 57848a9be0SAndreas Gohr $INPUT->str('2fa_provider'), 58848a9be0SAndreas Gohr $INPUT->bool('sticky') 59848a9be0SAndreas Gohr )) { 60a386a536SAndreas Gohr $event->data = 'show'; 61848a9be0SAndreas Gohr return; 62a386a536SAndreas Gohr } else { 63a386a536SAndreas Gohr // show form 64a386a536SAndreas Gohr $event->preventDefault(); 65a386a536SAndreas Gohr return; 66a386a536SAndreas Gohr } 67a386a536SAndreas Gohr } 68a386a536SAndreas Gohr 69*857c5abcSAndreas Gohr // clear cookie on logout 70*857c5abcSAndreas Gohr if ($event->data === 'logout') { 71*857c5abcSAndreas Gohr $this->deAuth(); 72*857c5abcSAndreas Gohr return; 73*857c5abcSAndreas Gohr } 74*857c5abcSAndreas Gohr 75a386a536SAndreas Gohr // authed already, continue 76a386a536SAndreas Gohr if ($this->isAuthed()) { 77a386a536SAndreas Gohr return; 78a386a536SAndreas Gohr } 79a386a536SAndreas Gohr 805f8f561aSAndreas Gohr if (count($manager->getUserProviders())) { 81a386a536SAndreas Gohr // user has already 2fa set up - they need to authenticate before anything else 82a386a536SAndreas Gohr $event->data = 'twofactor_login'; 83a386a536SAndreas Gohr $event->preventDefault(); 84a386a536SAndreas Gohr $event->stopPropagation(); 85a386a536SAndreas Gohr return; 86a386a536SAndreas Gohr } 87a386a536SAndreas Gohr 885f8f561aSAndreas Gohr if ($manager->isRequired()) { 89a386a536SAndreas Gohr // 2fa is required - they need to set it up now 90a386a536SAndreas Gohr // this will be handled by action/profile.php 91a386a536SAndreas Gohr $event->data = 'twofactor_profile'; 92a386a536SAndreas Gohr } 93a386a536SAndreas Gohr 94a386a536SAndreas Gohr // all good. proceed 95a386a536SAndreas Gohr } 96a386a536SAndreas Gohr 97a386a536SAndreas Gohr /** 98a386a536SAndreas Gohr * Show a 2fa login screen 99a386a536SAndreas Gohr * 100a386a536SAndreas Gohr * @param Doku_Event $event 101a386a536SAndreas Gohr */ 102a386a536SAndreas Gohr public function handleLoginDisplay(Doku_Event $event) 103a386a536SAndreas Gohr { 104a386a536SAndreas Gohr if ($event->data !== 'twofactor_login') return; 1055f8f561aSAndreas Gohr $manager = Manager::getInstance(); 1065f8f561aSAndreas Gohr if (!$manager->isReady()) return; 1075f8f561aSAndreas Gohr 108a386a536SAndreas Gohr $event->preventDefault(); 109a386a536SAndreas Gohr $event->stopPropagation(); 110a386a536SAndreas Gohr 111a386a536SAndreas Gohr global $INPUT; 112c9e42a8dSAndreas Gohr global $ID; 113c9e42a8dSAndreas Gohr 114a386a536SAndreas Gohr $providerID = $INPUT->str('2fa_provider'); 1155f8f561aSAndreas Gohr $providers = $manager->getUserProviders(); 1165f8f561aSAndreas Gohr $provider = $providers[$providerID] ?? $manager->getUserDefaultProvider(); 117b6119621SAndreas Gohr // remove current provider from list 118b6119621SAndreas Gohr unset($providers[$provider->getProviderID()]); 119a386a536SAndreas Gohr 120*857c5abcSAndreas Gohr echo $this->locale_xhtml('login'); 121848a9be0SAndreas Gohr $form = new dokuwiki\Form\Form(['method' => 'POST']); 122848a9be0SAndreas Gohr $form->setHiddenField('do', 'twofactor_login'); 123a386a536SAndreas Gohr $form->setHiddenField('2fa_provider', $provider->getProviderID()); 124a386a536SAndreas Gohr $form->addFieldsetOpen($provider->getLabel()); 125a386a536SAndreas Gohr try { 126a386a536SAndreas Gohr $code = $provider->generateCode(); 127a386a536SAndreas Gohr $info = $provider->transmitMessage($code); 128a386a536SAndreas Gohr $form->addHTML('<p>' . hsc($info) . '</p>'); 129848a9be0SAndreas Gohr $form->addTextInput('2fa_code', 'Your Code')->val(''); 130848a9be0SAndreas Gohr $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login 131a386a536SAndreas Gohr $form->addButton('2fa', 'Submit')->attr('type', 'submit'); 1325f8f561aSAndreas Gohr } catch (Exception $e) { 133a386a536SAndreas Gohr msg(hsc($e->getMessage()), -1); // FIXME better handling 134a386a536SAndreas Gohr } 135a386a536SAndreas Gohr $form->addFieldsetClose(); 136a386a536SAndreas Gohr 137a386a536SAndreas Gohr if (count($providers)) { 138a386a536SAndreas Gohr $form->addFieldsetOpen('Alternative methods'); 139a386a536SAndreas Gohr foreach ($providers as $prov) { 140c9e42a8dSAndreas Gohr $url = wl($ID, [ 141c9e42a8dSAndreas Gohr 'do' => 'twofactor_login', 142c9e42a8dSAndreas Gohr '2fa_provider' => $prov->getProviderID(), 143c9e42a8dSAndreas Gohr ]); 144c9e42a8dSAndreas Gohr $form->addHTML('< href="' . $url . '">' . hsc($prov->getLabel()) . '</a>'); 145a386a536SAndreas Gohr } 146a386a536SAndreas Gohr $form->addFieldsetClose(); 147a386a536SAndreas Gohr } 148a386a536SAndreas Gohr 149a386a536SAndreas Gohr echo $form->toHTML(); 150a386a536SAndreas Gohr } 151a386a536SAndreas Gohr 152a386a536SAndreas Gohr /** 153a386a536SAndreas Gohr * Has the user already authenticated with the second factor? 154a386a536SAndreas Gohr * @return bool 155a386a536SAndreas Gohr */ 156a386a536SAndreas Gohr protected function isAuthed() 157a386a536SAndreas Gohr { 158848a9be0SAndreas Gohr if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 159848a9be0SAndreas Gohr $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 160848a9be0SAndreas Gohr if (!is_array($data)) return false; 1616c996db8SAndreas Gohr list($providerID, $hash,) = $data; 162848a9be0SAndreas Gohr 163848a9be0SAndreas Gohr try { 1645f8f561aSAndreas Gohr $provider = (Manager::getInstance())->getUserProvider($providerID); 1656c996db8SAndreas Gohr if ($this->cookieHash($provider) !== $hash) return false; 166848a9be0SAndreas Gohr return true; 1675f8f561aSAndreas Gohr } catch (Exception $ignored) { 168a386a536SAndreas Gohr return false; 169a386a536SAndreas Gohr } 170848a9be0SAndreas Gohr } 171a386a536SAndreas Gohr 172a386a536SAndreas Gohr /** 173*857c5abcSAndreas Gohr * Deletes the cookie 174*857c5abcSAndreas Gohr * 175*857c5abcSAndreas Gohr * @return void 176*857c5abcSAndreas Gohr */ 177*857c5abcSAndreas Gohr protected function deAuth() 178*857c5abcSAndreas Gohr { 179*857c5abcSAndreas Gohr global $conf; 180*857c5abcSAndreas Gohr 181*857c5abcSAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 182*857c5abcSAndreas Gohr $time = time() - 60 * 60 * 24 * 365; // one year in the past 183*857c5abcSAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 184*857c5abcSAndreas Gohr } 185*857c5abcSAndreas Gohr 186*857c5abcSAndreas Gohr /** 187a386a536SAndreas Gohr * Verify a given code 188a386a536SAndreas Gohr * 189a386a536SAndreas Gohr * @return bool 190a386a536SAndreas Gohr * @throws Exception 191a386a536SAndreas Gohr */ 192848a9be0SAndreas Gohr protected function verify($code, $providerID, $sticky) 193a386a536SAndreas Gohr { 194848a9be0SAndreas Gohr global $conf; 195848a9be0SAndreas Gohr 196a386a536SAndreas Gohr if (!$code) return false; 197a386a536SAndreas Gohr if (!$providerID) return false; 1985f8f561aSAndreas Gohr $provider = (Manager::getInstance())->getUserProvider($providerID); 199a386a536SAndreas Gohr $ok = $provider->checkCode($code); 200848a9be0SAndreas Gohr if (!$ok) { 201848a9be0SAndreas Gohr msg('code was wrong', -1); 202848a9be0SAndreas Gohr return false; 203848a9be0SAndreas Gohr } 204a386a536SAndreas Gohr 205848a9be0SAndreas Gohr // store cookie 2066c996db8SAndreas Gohr $hash = $this->cookieHash($provider); 2076c996db8SAndreas Gohr $data = base64_encode(serialize([$providerID, $hash, time()])); 208848a9be0SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 209848a9be0SAndreas Gohr $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year 210848a9be0SAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 211a386a536SAndreas Gohr 212a386a536SAndreas Gohr return true; 213a386a536SAndreas Gohr } 2146c996db8SAndreas Gohr 2156c996db8SAndreas Gohr /** 2166c996db8SAndreas Gohr * Create a hash that validates the cookie 2176c996db8SAndreas Gohr * 2186c996db8SAndreas Gohr * @param Provider $provider 2196c996db8SAndreas Gohr * @return string 2206c996db8SAndreas Gohr */ 2216c996db8SAndreas Gohr protected function cookieHash($provider) 2226c996db8SAndreas Gohr { 2236c996db8SAndreas Gohr return sha1(join("\n", [ 2246c996db8SAndreas Gohr $provider->getProviderID(), 2255f8f561aSAndreas Gohr (Manager::getInstance())->getUser(), 2266c996db8SAndreas Gohr $provider->getSecret(), 2276c996db8SAndreas Gohr auth_browseruid(), 2286c996db8SAndreas Gohr auth_cookiesalt(false, true), 2296c996db8SAndreas Gohr ])); 2306c996db8SAndreas Gohr } 231fca58076SAndreas Gohr} 232