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