1fca58076SAndreas Gohr<?php 28b7620a8SAndreas Gohr 38bfbdf72SAndreas Gohruse dokuwiki\JWT; 48b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager; 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 { 55c8525a21SAndreas Gohr if ($event->data === 'resendpwd') { 56c8525a21SAndreas Gohr // this is completely handled in resendpwd.php 57c8525a21SAndreas Gohr return; 58c8525a21SAndreas Gohr } 59c8525a21SAndreas Gohr 605f8f561aSAndreas Gohr $manager = Manager::getInstance(); 615f8f561aSAndreas Gohr if (!$manager->isReady()) return; 62a386a536SAndreas Gohr 63a386a536SAndreas Gohr global $INPUT; 64a386a536SAndreas Gohr 65a386a536SAndreas Gohr // already in a 2fa login? 66a386a536SAndreas Gohr if ($event->data === 'twofactor_login') { 67848a9be0SAndreas Gohr if ($this->verify( 68848a9be0SAndreas Gohr $INPUT->str('2fa_code'), 69848a9be0SAndreas Gohr $INPUT->str('2fa_provider'), 7003bae0e0SAndreas Gohr $this->isSticky() 71848a9be0SAndreas Gohr )) { 72a386a536SAndreas Gohr $event->data = 'show'; 73848a9be0SAndreas Gohr return; 74a386a536SAndreas Gohr } else { 75a386a536SAndreas Gohr // show form 76a386a536SAndreas Gohr $event->preventDefault(); 77a386a536SAndreas Gohr return; 78a386a536SAndreas Gohr } 79a386a536SAndreas Gohr } 80a386a536SAndreas Gohr 81857c5abcSAndreas Gohr // clear cookie on logout 82857c5abcSAndreas Gohr if ($event->data === 'logout') { 83857c5abcSAndreas Gohr $this->deAuth(); 84857c5abcSAndreas Gohr return; 85857c5abcSAndreas Gohr } 86857c5abcSAndreas Gohr 87a386a536SAndreas Gohr // authed already, continue 88a386a536SAndreas Gohr if ($this->isAuthed()) { 89a386a536SAndreas Gohr return; 90a386a536SAndreas Gohr } 91a386a536SAndreas Gohr 925f8f561aSAndreas Gohr if (count($manager->getUserProviders())) { 93a386a536SAndreas Gohr // user has already 2fa set up - they need to authenticate before anything else 94a386a536SAndreas Gohr $event->data = 'twofactor_login'; 95a386a536SAndreas Gohr $event->preventDefault(); 96a386a536SAndreas Gohr $event->stopPropagation(); 97a386a536SAndreas Gohr return; 98a386a536SAndreas Gohr } 99a386a536SAndreas Gohr 1005f8f561aSAndreas Gohr if ($manager->isRequired()) { 101a386a536SAndreas Gohr // 2fa is required - they need to set it up now 102a386a536SAndreas Gohr // this will be handled by action/profile.php 103a386a536SAndreas Gohr $event->data = 'twofactor_profile'; 104a386a536SAndreas Gohr } 105a386a536SAndreas Gohr 106a386a536SAndreas Gohr // all good. proceed 107a386a536SAndreas Gohr } 108a386a536SAndreas Gohr 109a386a536SAndreas Gohr /** 110a386a536SAndreas Gohr * Show a 2fa login screen 111a386a536SAndreas Gohr * 112a386a536SAndreas Gohr * @param Doku_Event $event 113a386a536SAndreas Gohr */ 114a386a536SAndreas Gohr public function handleLoginDisplay(Doku_Event $event) 115a386a536SAndreas Gohr { 116a386a536SAndreas Gohr if ($event->data !== 'twofactor_login') return; 1175f8f561aSAndreas Gohr $manager = Manager::getInstance(); 1185f8f561aSAndreas Gohr if (!$manager->isReady()) return; 1195f8f561aSAndreas Gohr 120a386a536SAndreas Gohr $event->preventDefault(); 121a386a536SAndreas Gohr $event->stopPropagation(); 122a386a536SAndreas Gohr 123a386a536SAndreas Gohr global $INPUT; 124a386a536SAndreas Gohr $providerID = $INPUT->str('2fa_provider'); 125a386a536SAndreas Gohr 1260407d282SAndreas Gohr echo '<div class="plugin_twofactor_login">'; 1270407d282SAndreas Gohr echo inlineSVG(__DIR__ . '/../admin.svg'); 128857c5abcSAndreas Gohr echo $this->locale_xhtml('login'); 129c8525a21SAndreas Gohr echo $manager->getCodeForm($providerID)->toHTML(); 1300407d282SAndreas Gohr echo '</div>'; 131a386a536SAndreas Gohr } 132a386a536SAndreas Gohr 133a386a536SAndreas Gohr /** 134210d81e3SAndreas Gohr * Remove user info from non-main entry points while we wait for 2fa 135210d81e3SAndreas Gohr * 136210d81e3SAndreas Gohr * @param Doku_Event $event 137210d81e3SAndreas Gohr */ 138210d81e3SAndreas Gohr public function handleInitDone(Doku_Event $event) 139210d81e3SAndreas Gohr { 140210d81e3SAndreas Gohr global $INPUT; 1418bfbdf72SAndreas Gohr $script = basename($INPUT->server->str('SCRIPT_NAME')); 142210d81e3SAndreas Gohr 143210d81e3SAndreas Gohr if (!(Manager::getInstance())->isReady()) return; 1448bfbdf72SAndreas Gohr if ($script == DOKU_SCRIPT) return; 145210d81e3SAndreas Gohr if ($this->isAuthed()) return; 146210d81e3SAndreas Gohr 1470d5f8055SAnna Dabrowska if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return; 1480d5f8055SAnna Dabrowska 1498bfbdf72SAndreas Gohr // allow API access without 2fa when using token auth 150*afea3ddaSAndreas Gohr if(in_array($script, ['xmlrpc.php', 'jsonrpc.php']) || $this->getConf('allowTokenAuth')) { 1518bfbdf72SAndreas Gohr if ($this->hasValidTokenAuth()) return; 1528bfbdf72SAndreas Gohr } 1538bfbdf72SAndreas Gohr 154210d81e3SAndreas Gohr // temporarily remove user info from environment 155210d81e3SAndreas Gohr $INPUT->server->remove('REMOTE_USER'); 156210d81e3SAndreas Gohr unset($_SESSION[DOKU_COOKIE]['auth']); 157210d81e3SAndreas Gohr unset($GLOBALS['USERINFO']); 158210d81e3SAndreas Gohr } 159210d81e3SAndreas Gohr 160210d81e3SAndreas Gohr /** 161a386a536SAndreas Gohr * Has the user already authenticated with the second factor? 162a386a536SAndreas Gohr * @return bool 163a386a536SAndreas Gohr */ 164a386a536SAndreas Gohr protected function isAuthed() 165a386a536SAndreas Gohr { 16695ed3afaSAndreas Gohr // if we trust the IP, we don't need 2fa and consider the user authed automatically 16795ed3afaSAndreas Gohr if ( 16895ed3afaSAndreas Gohr $this->getConf('trustedIPs') && 16995ed3afaSAndreas Gohr preg_match('/' . $this->getConf('trustedIPs') . '/', clientIP(true)) 17095ed3afaSAndreas Gohr ) { 17195ed3afaSAndreas Gohr return true; 17295ed3afaSAndreas Gohr } 17395ed3afaSAndreas Gohr 174848a9be0SAndreas Gohr if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 175848a9be0SAndreas Gohr $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 176848a9be0SAndreas Gohr if (!is_array($data)) return false; 1776c996db8SAndreas Gohr list($providerID, $hash,) = $data; 178848a9be0SAndreas Gohr 179848a9be0SAndreas Gohr try { 1805f8f561aSAndreas Gohr $provider = (Manager::getInstance())->getUserProvider($providerID); 1816c996db8SAndreas Gohr if ($this->cookieHash($provider) !== $hash) return false; 182848a9be0SAndreas Gohr return true; 1835f8f561aSAndreas Gohr } catch (Exception $ignored) { 184a386a536SAndreas Gohr return false; 185a386a536SAndreas Gohr } 186848a9be0SAndreas Gohr } 187a386a536SAndreas Gohr 188a386a536SAndreas Gohr /** 18903bae0e0SAndreas Gohr * Get sticky value from standard cookie 19003bae0e0SAndreas Gohr * 19103bae0e0SAndreas Gohr * @return bool 19203bae0e0SAndreas Gohr */ 19303bae0e0SAndreas Gohr protected function isSticky() 19403bae0e0SAndreas Gohr { 19503bae0e0SAndreas Gohr if (!isset($_COOKIE[DOKU_COOKIE])) { 19603bae0e0SAndreas Gohr return false; 19703bae0e0SAndreas Gohr } 19803bae0e0SAndreas Gohr list(, $sticky,) = explode('|', $_COOKIE[DOKU_COOKIE], 3); 19903bae0e0SAndreas Gohr return (bool)$sticky; 20003bae0e0SAndreas Gohr } 20103bae0e0SAndreas Gohr 20203bae0e0SAndreas Gohr /** 203857c5abcSAndreas Gohr * Deletes the cookie 204857c5abcSAndreas Gohr * 205857c5abcSAndreas Gohr * @return void 206857c5abcSAndreas Gohr */ 207857c5abcSAndreas Gohr protected function deAuth() 208857c5abcSAndreas Gohr { 209857c5abcSAndreas Gohr global $conf; 210857c5abcSAndreas Gohr 211857c5abcSAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 212857c5abcSAndreas Gohr $time = time() - 60 * 60 * 24 * 365; // one year in the past 213857c5abcSAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 214857c5abcSAndreas Gohr } 215857c5abcSAndreas Gohr 216857c5abcSAndreas Gohr /** 217a386a536SAndreas Gohr * Verify a given code 218a386a536SAndreas Gohr * 219a386a536SAndreas Gohr * @return bool 220a386a536SAndreas Gohr * @throws Exception 221a386a536SAndreas Gohr */ 222848a9be0SAndreas Gohr protected function verify($code, $providerID, $sticky) 223a386a536SAndreas Gohr { 224848a9be0SAndreas Gohr global $conf; 225848a9be0SAndreas Gohr 226c8525a21SAndreas Gohr $manager = Manager::getInstance(); 227c8525a21SAndreas Gohr if (!$manager->verifyCode($code, $providerID)) return false; 228c8525a21SAndreas Gohr 2295f8f561aSAndreas Gohr $provider = (Manager::getInstance())->getUserProvider($providerID); 230a386a536SAndreas Gohr 231848a9be0SAndreas Gohr // store cookie 2326c996db8SAndreas Gohr $hash = $this->cookieHash($provider); 2336c996db8SAndreas Gohr $data = base64_encode(serialize([$providerID, $hash, time()])); 234848a9be0SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 23503bae0e0SAndreas Gohr $time = $sticky ? (time() + 60 * 60 * 24 * 30 * 3) : 0; //three months on sticky login 236848a9be0SAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 237a386a536SAndreas Gohr 238a386a536SAndreas Gohr return true; 239a386a536SAndreas Gohr } 2406c996db8SAndreas Gohr 2416c996db8SAndreas Gohr /** 2426c996db8SAndreas Gohr * Create a hash that validates the cookie 2436c996db8SAndreas Gohr * 2446c996db8SAndreas Gohr * @param Provider $provider 2456c996db8SAndreas Gohr * @return string 2466c996db8SAndreas Gohr */ 2476c996db8SAndreas Gohr protected function cookieHash($provider) 2486c996db8SAndreas Gohr { 2496c996db8SAndreas Gohr return sha1(join("\n", [ 2506c996db8SAndreas Gohr $provider->getProviderID(), 2515f8f561aSAndreas Gohr (Manager::getInstance())->getUser(), 2526c996db8SAndreas Gohr $provider->getSecret(), 25309c2ba1aSalexdraconian $this->getConf("useinternaluid") ? auth_browseruid() : $_SERVER['HTTP_USER_AGENT'], 2546c996db8SAndreas Gohr auth_cookiesalt(false, true), 2556c996db8SAndreas Gohr ])); 2566c996db8SAndreas Gohr } 2578bfbdf72SAndreas Gohr 2588bfbdf72SAndreas Gohr /** 2598bfbdf72SAndreas Gohr * Check if the user has a valid auth token. We might skip 2fa for them. 2608bfbdf72SAndreas Gohr * 2618bfbdf72SAndreas Gohr * This duplicates code from auth_tokenlogin() until DokuWiki has a proper mechanism to access the token 2628bfbdf72SAndreas Gohr * 2638bfbdf72SAndreas Gohr * @return bool 2648bfbdf72SAndreas Gohr */ 2658bfbdf72SAndreas Gohr protected function hasValidTokenAuth() 2668bfbdf72SAndreas Gohr { 2678bfbdf72SAndreas Gohr $headers = []; 2688bfbdf72SAndreas Gohr 2698bfbdf72SAndreas Gohr // try to get the headers from Apache 2708bfbdf72SAndreas Gohr if (function_exists('getallheaders')) { 2718bfbdf72SAndreas Gohr $headers = getallheaders(); 2728bfbdf72SAndreas Gohr if (is_array($headers)) { 2738bfbdf72SAndreas Gohr $headers = array_change_key_case($headers); 2748bfbdf72SAndreas Gohr } 2758bfbdf72SAndreas Gohr } 2768bfbdf72SAndreas Gohr 2778bfbdf72SAndreas Gohr // get the headers from $_SERVER 2788bfbdf72SAndreas Gohr if (!$headers) { 2798bfbdf72SAndreas Gohr foreach ($_SERVER as $key => $value) { 2808bfbdf72SAndreas Gohr if (substr($key, 0, 5) === 'HTTP_') { 2818bfbdf72SAndreas Gohr $headers[strtolower(substr($key, 5))] = $value; 2828bfbdf72SAndreas Gohr } 2838bfbdf72SAndreas Gohr } 2848bfbdf72SAndreas Gohr } 2858bfbdf72SAndreas Gohr 2868bfbdf72SAndreas Gohr // check authorization header 2878bfbdf72SAndreas Gohr if (isset($headers['authorization'])) { 2888bfbdf72SAndreas Gohr [$type, $token] = sexplode(' ', $headers['authorization'], 2); 2898bfbdf72SAndreas Gohr if ($type !== 'Bearer') $token = ''; // not the token we want 2908bfbdf72SAndreas Gohr } 2918bfbdf72SAndreas Gohr 2928bfbdf72SAndreas Gohr // check x-dokuwiki-token header 2938bfbdf72SAndreas Gohr if (isset($headers['x-dokuwiki-token'])) { 2948bfbdf72SAndreas Gohr $token = $headers['x-dokuwiki-token']; 2958bfbdf72SAndreas Gohr } 2968bfbdf72SAndreas Gohr 2978bfbdf72SAndreas Gohr if (empty($token)) return false; 2988bfbdf72SAndreas Gohr 2998bfbdf72SAndreas Gohr // check token 3008bfbdf72SAndreas Gohr try { 3018bfbdf72SAndreas Gohr JWT::validate($token); 3028bfbdf72SAndreas Gohr } catch (Exception $e) { 3038bfbdf72SAndreas Gohr return false; 3048bfbdf72SAndreas Gohr } 3058bfbdf72SAndreas Gohr return true; 3068bfbdf72SAndreas Gohr } 307fca58076SAndreas Gohr} 308