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