1<?php 2 3use dokuwiki\plugin\twofactor\Manager; 4use dokuwiki\plugin\twofactor\Provider; 5 6/** 7 * DokuWiki Plugin twofactor (Action Component) 8 * 9 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 10 */ 11class action_plugin_twofactor_login extends DokuWiki_Action_Plugin 12{ 13 const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE; 14 15 /** 16 * Registers the event handlers. 17 */ 18 public function register(Doku_Event_Handler $controller) 19 { 20 // check 2fa requirements and either move to profile or login handling 21 $controller->register_hook( 22 'ACTION_ACT_PREPROCESS', 23 'BEFORE', 24 $this, 25 'handleActionPreProcess', 26 null, 27 Manager::EVENT_PRIORITY 28 ); 29 30 // display login form 31 $controller->register_hook( 32 'TPL_ACT_UNKNOWN', 33 'BEFORE', 34 $this, 35 'handleLoginDisplay' 36 ); 37 38 // disable user in all non-main screens (media, detail, ajax, ...) 39 $controller->register_hook( 40 'DOKUWIKI_INIT_DONE', 41 'BEFORE', 42 $this, 43 'handleInitDone' 44 ); 45 } 46 47 /** 48 * Decide if any 2fa handling needs to be done for the current user 49 * 50 * @param Doku_Event $event 51 */ 52 public function handleActionPreProcess(Doku_Event $event) 53 { 54 if ($event->data === 'resendpwd') { 55 // this is completely handled in resendpwd.php 56 return; 57 } 58 59 $manager = Manager::getInstance(); 60 if (!$manager->isReady()) return; 61 62 global $INPUT; 63 64 // already in a 2fa login? 65 if ($event->data === 'twofactor_login') { 66 if ($this->verify( 67 $INPUT->str('2fa_code'), 68 $INPUT->str('2fa_provider'), 69 $this->isSticky() 70 )) { 71 $event->data = 'show'; 72 return; 73 } else { 74 // show form 75 $event->preventDefault(); 76 return; 77 } 78 } 79 80 // clear cookie on logout 81 if ($event->data === 'logout') { 82 $this->deAuth(); 83 return; 84 } 85 86 // authed already, continue 87 if ($this->isAuthed()) { 88 return; 89 } 90 91 if (count($manager->getUserProviders())) { 92 // user has already 2fa set up - they need to authenticate before anything else 93 $event->data = 'twofactor_login'; 94 $event->preventDefault(); 95 $event->stopPropagation(); 96 return; 97 } 98 99 if ($manager->isRequired()) { 100 // 2fa is required - they need to set it up now 101 // this will be handled by action/profile.php 102 $event->data = 'twofactor_profile'; 103 } 104 105 // all good. proceed 106 } 107 108 /** 109 * Show a 2fa login screen 110 * 111 * @param Doku_Event $event 112 */ 113 public function handleLoginDisplay(Doku_Event $event) 114 { 115 if ($event->data !== 'twofactor_login') return; 116 $manager = Manager::getInstance(); 117 if (!$manager->isReady()) return; 118 119 $event->preventDefault(); 120 $event->stopPropagation(); 121 122 global $INPUT; 123 $providerID = $INPUT->str('2fa_provider'); 124 125 echo '<div class="plugin_twofactor_login">'; 126 echo inlineSVG(__DIR__ . '/../admin.svg'); 127 echo $this->locale_xhtml('login'); 128 echo $manager->getCodeForm($providerID)->toHTML(); 129 echo '</div>'; 130 } 131 132 /** 133 * Remove user info from non-main entry points while we wait for 2fa 134 * 135 * @param Doku_Event $event 136 */ 137 public function handleInitDone(Doku_Event $event) 138 { 139 global $INPUT; 140 141 if (!(Manager::getInstance())->isReady()) return; 142 if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return; 143 if ($this->isAuthed()) return; 144 145 if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return; 146 147 // temporarily remove user info from environment 148 $INPUT->server->remove('REMOTE_USER'); 149 unset($_SESSION[DOKU_COOKIE]['auth']); 150 unset($GLOBALS['USERINFO']); 151 } 152 153 /** 154 * Has the user already authenticated with the second factor? 155 * @return bool 156 */ 157 protected function isAuthed() 158 { 159 // if we trust the IP, we don't need 2fa and consider the user authed automatically 160 if ( 161 $this->getConf('trustedIPs') && 162 preg_match('/' . $this->getConf('trustedIPs') . '/', clientIP(true)) 163 ) { 164 return true; 165 } 166 167 if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 168 $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 169 if (!is_array($data)) return false; 170 list($providerID, $hash,) = $data; 171 172 try { 173 $provider = (Manager::getInstance())->getUserProvider($providerID); 174 if ($this->cookieHash($provider) !== $hash) return false; 175 return true; 176 } catch (Exception $ignored) { 177 return false; 178 } 179 } 180 181 /** 182 * Get sticky value from standard cookie 183 * 184 * @return bool 185 */ 186 protected function isSticky() 187 { 188 if (!isset($_COOKIE[DOKU_COOKIE])) { 189 return false; 190 } 191 list(, $sticky,) = explode('|', $_COOKIE[DOKU_COOKIE], 3); 192 return (bool)$sticky; 193 } 194 195 /** 196 * Deletes the cookie 197 * 198 * @return void 199 */ 200 protected function deAuth() 201 { 202 global $conf; 203 204 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 205 $time = time() - 60 * 60 * 24 * 365; // one year in the past 206 setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 207 } 208 209 /** 210 * Verify a given code 211 * 212 * @return bool 213 * @throws Exception 214 */ 215 protected function verify($code, $providerID, $sticky) 216 { 217 global $conf; 218 219 $manager = Manager::getInstance(); 220 if (!$manager->verifyCode($code, $providerID)) return false; 221 222 $provider = (Manager::getInstance())->getUserProvider($providerID); 223 224 // store cookie 225 $hash = $this->cookieHash($provider); 226 $data = base64_encode(serialize([$providerID, $hash, time()])); 227 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 228 $time = $sticky ? (time() + 60 * 60 * 24 * 30 * 3) : 0; //three months on sticky login 229 setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 230 231 return true; 232 } 233 234 /** 235 * Create a hash that validates the cookie 236 * 237 * @param Provider $provider 238 * @return string 239 */ 240 protected function cookieHash($provider) 241 { 242 return sha1(join("\n", [ 243 $provider->getProviderID(), 244 (Manager::getInstance())->getUser(), 245 $provider->getSecret(), 246 $this->getConf("useinternaluid") ? auth_browseruid() : $_SERVER['HTTP_USER_AGENT'], 247 auth_cookiesalt(false, true), 248 ])); 249 } 250} 251