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