1<?php 2 3use dokuwiki\plugin\twofactor\Manager; 4 5/** 6 * DokuWiki Plugin twofactor (Action Component) 7 * 8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 9 */ 10class action_plugin_twofactor_login extends DokuWiki_Action_Plugin 11{ 12 const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE; 13 14 /** @var Manager */ 15 protected $manager; 16 17 /** 18 * Constructor 19 */ 20 public function __construct() 21 { 22 $this->manager = Manager::getInstance(); 23 } 24 25 /** 26 * Registers the event handlers. 27 */ 28 public function register(Doku_Event_Handler $controller) 29 { 30 if (!(Manager::getInstance())->isReady()) return; 31 32 // check 2fa requirements and either move to profile or login handling 33 $controller->register_hook( 34 'ACTION_ACT_PREPROCESS', 35 'BEFORE', 36 $this, 37 'handleActionPreProcess', 38 null, 39 -999999 40 ); 41 42 // display login form 43 $controller->register_hook( 44 'TPL_ACT_UNKNOWN', 45 'BEFORE', 46 $this, 47 'handleLoginDisplay' 48 ); 49 50 // FIXME disable user in all non-main screens (media, detail, ajax, ...) 51 } 52 53 /** 54 * Decide if any 2fa handling needs to be done for the current user 55 * 56 * @param Doku_Event $event 57 */ 58 public function handleActionPreProcess(Doku_Event $event) 59 { 60 if (!$this->manager->getUser()) 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 $INPUT->bool('sticky') 70 )) { 71 $event->data = 'show'; 72 return; 73 } else { 74 // show form 75 $event->preventDefault(); 76 return; 77 } 78 } 79 80 // authed already, continue 81 if ($this->isAuthed()) { 82 return; 83 } 84 85 if (count($this->manager->getUserProviders())) { 86 // user has already 2fa set up - they need to authenticate before anything else 87 $event->data = 'twofactor_login'; 88 $event->preventDefault(); 89 $event->stopPropagation(); 90 return; 91 } 92 93 if ($this->manager->isRequired()) { 94 // 2fa is required - they need to set it up now 95 // this will be handled by action/profile.php 96 $event->data = 'twofactor_profile'; 97 } 98 99 // all good. proceed 100 } 101 102 /** 103 * Show a 2fa login screen 104 * 105 * @param Doku_Event $event 106 */ 107 public function handleLoginDisplay(Doku_Event $event) 108 { 109 if ($event->data !== 'twofactor_login') return; 110 $event->preventDefault(); 111 $event->stopPropagation(); 112 113 global $INPUT; 114 $providerID = $INPUT->str('2fa_provider'); 115 $providers = $this->manager->getUserProviders(); 116 if (isset($providers[$providerID])) { 117 $provider = $providers[$providerID]; 118 } else { 119 $provider = $this->manager->getUserDefaultProvider(); 120 } 121 // remove current provider from list 122 unset($providers[$provider->getProviderID()]); 123 124 $form = new dokuwiki\Form\Form(['method' => 'POST']); 125 $form->setHiddenField('do', 'twofactor_login'); 126 $form->setHiddenField('2fa_provider', $provider->getProviderID()); 127 $form->addFieldsetOpen($provider->getLabel()); 128 try { 129 $code = $provider->generateCode(); 130 $info = $provider->transmitMessage($code); 131 $form->addHTML('<p>' . hsc($info) . '</p>'); 132 $form->addTextInput('2fa_code', 'Your Code')->val(''); 133 $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login 134 $form->addButton('2fa', 'Submit')->attr('type', 'submit'); 135 } catch (\Exception $e) { 136 msg(hsc($e->getMessage()), -1); // FIXME better handling 137 } 138 $form->addFieldsetClose(); 139 140 if (count($providers)) { 141 $form->addFieldsetOpen('Alternative methods'); 142 foreach ($providers as $prov) { 143 $link = $prov->getProviderID(); // FIXME build correct links 144 145 $form->addHTML($link); 146 } 147 $form->addFieldsetClose(); 148 } 149 150 echo $form->toHTML(); 151 } 152 153 /** 154 * Has the user already authenticated with the second factor? 155 * @return bool 156 */ 157 protected function isAuthed() 158 { 159 if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 160 $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 161 if (!is_array($data)) return false; 162 list($providerID, $buid,) = $data; 163 if (auth_browseruid() !== $buid) return false; 164 165 try { 166 // ensure it's a still valid provider 167 $this->manager->getUserProvider($providerID); 168 return true; 169 } catch (\Exception $e) { 170 return false; 171 } 172 } 173 174 /** 175 * Verify a given code 176 * 177 * @return bool 178 * @throws Exception 179 */ 180 protected function verify($code, $providerID, $sticky) 181 { 182 global $conf; 183 184 if (!$code) return false; 185 if (!$providerID) return false; 186 $provider = $this->manager->getUserProvider($providerID); 187 $ok = $provider->checkCode($code); 188 if (!$ok) { 189 msg('code was wrong', -1); 190 return false; 191 } 192 193 // store cookie 194 $data = base64_encode(serialize([$providerID, auth_browseruid(), time()])); 195 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 196 $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year 197 setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 198 199 return true; 200 } 201} 202