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 $manager = Manager::getInstance(); 55 if (!$manager->isReady()) return; 56 57 global $INPUT; 58 59 // already in a 2fa login? 60 if ($event->data === 'twofactor_login') { 61 if ($this->verify( 62 $INPUT->str('2fa_code'), 63 $INPUT->str('2fa_provider'), 64 $INPUT->bool('sticky') 65 )) { 66 $event->data = 'show'; 67 return; 68 } else { 69 // show form 70 $event->preventDefault(); 71 return; 72 } 73 } 74 75 // clear cookie on logout 76 if ($event->data === 'logout') { 77 $this->deAuth(); 78 return; 79 } 80 81 // authed already, continue 82 if ($this->isAuthed()) { 83 return; 84 } 85 86 if (count($manager->getUserProviders())) { 87 // user has already 2fa set up - they need to authenticate before anything else 88 $event->data = 'twofactor_login'; 89 $event->preventDefault(); 90 $event->stopPropagation(); 91 return; 92 } 93 94 if ($manager->isRequired()) { 95 // 2fa is required - they need to set it up now 96 // this will be handled by action/profile.php 97 $event->data = 'twofactor_profile'; 98 } 99 100 // all good. proceed 101 } 102 103 /** 104 * Show a 2fa login screen 105 * 106 * @param Doku_Event $event 107 */ 108 public function handleLoginDisplay(Doku_Event $event) 109 { 110 if ($event->data !== 'twofactor_login') return; 111 $manager = Manager::getInstance(); 112 if (!$manager->isReady()) return; 113 114 $event->preventDefault(); 115 $event->stopPropagation(); 116 117 global $INPUT; 118 global $ID; 119 120 $providerID = $INPUT->str('2fa_provider'); 121 $providers = $manager->getUserProviders(); 122 $provider = $providers[$providerID] ?? $manager->getUserDefaultProvider(); 123 // remove current provider from list 124 unset($providers[$provider->getProviderID()]); 125 126 echo '<div class="plugin_twofactor_login">'; 127 echo inlineSVG(__DIR__ . '/../admin.svg'); 128 echo $this->locale_xhtml('login'); 129 $form = new dokuwiki\Form\Form(['method' => 'POST']); 130 $form->setHiddenField('do', 'twofactor_login'); 131 $form->setHiddenField('2fa_provider', $provider->getProviderID()); 132 $form->addFieldsetOpen($provider->getLabel()); 133 try { 134 $code = $provider->generateCode(); 135 $info = $provider->transmitMessage($code); 136 $form->addHTML('<p>' . hsc($info) . '</p>'); 137 $form->addTextInput('2fa_code', 'Your Code')->val(''); 138 $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login 139 $form->addTagOpen('div')->addClass('buttons'); 140 $form->addButton('2fa', 'Submit')->attr('type', 'submit'); 141 $form->addTagClose('div'); 142 } catch (Exception $e) { 143 msg(hsc($e->getMessage()), -1); // FIXME better handling 144 } 145 $form->addFieldsetClose(); 146 147 if (count($providers)) { 148 $form->addFieldsetOpen('Alternative methods'); 149 foreach ($providers as $prov) { 150 $url = wl($ID, [ 151 'do' => 'twofactor_login', 152 '2fa_provider' => $prov->getProviderID(), 153 ]); 154 $form->addHTML('<a href="' . $url . '">' . hsc($prov->getLabel()) . '</a>'); 155 } 156 $form->addFieldsetClose(); 157 } 158 159 echo $form->toHTML(); 160 echo '</div>'; 161 } 162 163 /** 164 * Remove user info from non-main entry points while we wait for 2fa 165 * 166 * @param Doku_Event $event 167 */ 168 public function handleInitDone(Doku_Event $event) 169 { 170 global $INPUT; 171 172 if (!(Manager::getInstance())->isReady()) return; 173 if (basename($INPUT->server->str('SCRIPT_NAME')) == DOKU_SCRIPT) return; 174 if ($this->isAuthed()) return; 175 176 if ($this->getConf('optinout') !== 'mandatory' && empty(Manager::getInstance()->getUserProviders())) return; 177 178 // temporarily remove user info from environment 179 $INPUT->server->remove('REMOTE_USER'); 180 unset($_SESSION[DOKU_COOKIE]['auth']); 181 unset($GLOBALS['USERINFO']); 182 } 183 184 /** 185 * Has the user already authenticated with the second factor? 186 * @return bool 187 */ 188 protected function isAuthed() 189 { 190 if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 191 $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 192 if (!is_array($data)) return false; 193 list($providerID, $hash,) = $data; 194 195 try { 196 $provider = (Manager::getInstance())->getUserProvider($providerID); 197 if ($this->cookieHash($provider) !== $hash) return false; 198 return true; 199 } catch (Exception $ignored) { 200 return false; 201 } 202 } 203 204 /** 205 * Deletes the cookie 206 * 207 * @return void 208 */ 209 protected function deAuth() 210 { 211 global $conf; 212 213 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 214 $time = time() - 60 * 60 * 24 * 365; // one year in the past 215 setcookie(self::TWOFACTOR_COOKIE, null, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 216 } 217 218 /** 219 * Verify a given code 220 * 221 * @return bool 222 * @throws Exception 223 */ 224 protected function verify($code, $providerID, $sticky) 225 { 226 global $conf; 227 228 if (!$code) return false; 229 if (!$providerID) return false; 230 $provider = (Manager::getInstance())->getUserProvider($providerID); 231 $ok = $provider->checkCode($code); 232 if (!$ok) return false; 233 234 // store cookie 235 $hash = $this->cookieHash($provider); 236 $data = base64_encode(serialize([$providerID, $hash, time()])); 237 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 238 $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year 239 setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 240 241 return true; 242 } 243 244 /** 245 * Create a hash that validates the cookie 246 * 247 * @param Provider $provider 248 * @return string 249 */ 250 protected function cookieHash($provider) 251 { 252 return sha1(join("\n", [ 253 $provider->getProviderID(), 254 (Manager::getInstance())->getUser(), 255 $provider->getSecret(), 256 auth_browseruid(), 257 auth_cookiesalt(false, true), 258 ])); 259 } 260} 261