1fca58076SAndreas Gohr<?php 28b7620a8SAndreas Gohr 38b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager; 46c996db8SAndreas Gohruse dokuwiki\plugin\twofactor\Provider; 58b7620a8SAndreas Gohr 630625b49SAndreas Gohr/** 730625b49SAndreas Gohr * DokuWiki Plugin twofactor (Action Component) 830625b49SAndreas Gohr * 930625b49SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 1030625b49SAndreas Gohr */ 11fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin 12fca58076SAndreas Gohr{ 13848a9be0SAndreas Gohr const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE; 14848a9be0SAndreas Gohr 15a386a536SAndreas Gohr /** @var Manager */ 16a386a536SAndreas Gohr protected $manager; 17fca58076SAndreas Gohr 18a386a536SAndreas Gohr /** 19a386a536SAndreas Gohr * Constructor 20a386a536SAndreas Gohr */ 21fca58076SAndreas Gohr public function __construct() 22fca58076SAndreas Gohr { 23a386a536SAndreas Gohr $this->manager = Manager::getInstance(); 24fca58076SAndreas Gohr } 25fca58076SAndreas Gohr 26fca58076SAndreas Gohr /** 27fca58076SAndreas Gohr * Registers the event handlers. 28fca58076SAndreas Gohr */ 29fca58076SAndreas Gohr public function register(Doku_Event_Handler $controller) 30fca58076SAndreas Gohr { 318b7620a8SAndreas Gohr if (!(Manager::getInstance())->isReady()) return; 328b7620a8SAndreas Gohr 33a386a536SAndreas Gohr // check 2fa requirements and either move to profile or login handling 34a386a536SAndreas Gohr $controller->register_hook( 35a386a536SAndreas Gohr 'ACTION_ACT_PREPROCESS', 36a386a536SAndreas Gohr 'BEFORE', 37a386a536SAndreas Gohr $this, 38a386a536SAndreas Gohr 'handleActionPreProcess', 39a386a536SAndreas Gohr null, 40a386a536SAndreas Gohr -999999 41a386a536SAndreas Gohr ); 42fca58076SAndreas Gohr 43a386a536SAndreas Gohr // display login form 44a386a536SAndreas Gohr $controller->register_hook( 45a386a536SAndreas Gohr 'TPL_ACT_UNKNOWN', 46a386a536SAndreas Gohr 'BEFORE', 47a386a536SAndreas Gohr $this, 48a386a536SAndreas Gohr 'handleLoginDisplay' 49a386a536SAndreas Gohr ); 50a386a536SAndreas Gohr 51a386a536SAndreas Gohr // FIXME disable user in all non-main screens (media, detail, ajax, ...) 52fca58076SAndreas Gohr } 53fca58076SAndreas Gohr 54fca58076SAndreas Gohr /** 55a386a536SAndreas Gohr * Decide if any 2fa handling needs to be done for the current user 56a386a536SAndreas Gohr * 57a386a536SAndreas Gohr * @param Doku_Event $event 58a386a536SAndreas Gohr */ 59a386a536SAndreas Gohr public function handleActionPreProcess(Doku_Event $event) 60a386a536SAndreas Gohr { 61a386a536SAndreas Gohr if (!$this->manager->getUser()) return; 62a386a536SAndreas Gohr 63a386a536SAndreas Gohr global $INPUT; 64a386a536SAndreas Gohr 65a386a536SAndreas Gohr // already in a 2fa login? 66a386a536SAndreas Gohr if ($event->data === 'twofactor_login') { 67848a9be0SAndreas Gohr if ($this->verify( 68848a9be0SAndreas Gohr $INPUT->str('2fa_code'), 69848a9be0SAndreas Gohr $INPUT->str('2fa_provider'), 70848a9be0SAndreas Gohr $INPUT->bool('sticky') 71848a9be0SAndreas Gohr )) { 72a386a536SAndreas Gohr $event->data = 'show'; 73848a9be0SAndreas Gohr return; 74a386a536SAndreas Gohr } else { 75a386a536SAndreas Gohr // show form 76a386a536SAndreas Gohr $event->preventDefault(); 77a386a536SAndreas Gohr return; 78a386a536SAndreas Gohr } 79a386a536SAndreas Gohr } 80a386a536SAndreas Gohr 81a386a536SAndreas Gohr // authed already, continue 82a386a536SAndreas Gohr if ($this->isAuthed()) { 83a386a536SAndreas Gohr return; 84a386a536SAndreas Gohr } 85a386a536SAndreas Gohr 86a386a536SAndreas Gohr if (count($this->manager->getUserProviders())) { 87a386a536SAndreas Gohr // user has already 2fa set up - they need to authenticate before anything else 88a386a536SAndreas Gohr $event->data = 'twofactor_login'; 89a386a536SAndreas Gohr $event->preventDefault(); 90a386a536SAndreas Gohr $event->stopPropagation(); 91a386a536SAndreas Gohr return; 92a386a536SAndreas Gohr } 93a386a536SAndreas Gohr 94a386a536SAndreas Gohr if ($this->manager->isRequired()) { 95a386a536SAndreas Gohr // 2fa is required - they need to set it up now 96a386a536SAndreas Gohr // this will be handled by action/profile.php 97a386a536SAndreas Gohr $event->data = 'twofactor_profile'; 98a386a536SAndreas Gohr } 99a386a536SAndreas Gohr 100a386a536SAndreas Gohr // all good. proceed 101a386a536SAndreas Gohr } 102a386a536SAndreas Gohr 103a386a536SAndreas Gohr /** 104a386a536SAndreas Gohr * Show a 2fa login screen 105a386a536SAndreas Gohr * 106a386a536SAndreas Gohr * @param Doku_Event $event 107a386a536SAndreas Gohr */ 108a386a536SAndreas Gohr public function handleLoginDisplay(Doku_Event $event) 109a386a536SAndreas Gohr { 110a386a536SAndreas Gohr if ($event->data !== 'twofactor_login') return; 111a386a536SAndreas Gohr $event->preventDefault(); 112a386a536SAndreas Gohr $event->stopPropagation(); 113a386a536SAndreas Gohr 114a386a536SAndreas Gohr global $INPUT; 115*c9e42a8dSAndreas Gohr global $ID; 116*c9e42a8dSAndreas Gohr 117a386a536SAndreas Gohr $providerID = $INPUT->str('2fa_provider'); 118a386a536SAndreas Gohr $providers = $this->manager->getUserProviders(); 119a386a536SAndreas Gohr if (isset($providers[$providerID])) { 120a386a536SAndreas Gohr $provider = $providers[$providerID]; 121a386a536SAndreas Gohr } else { 122b6119621SAndreas Gohr $provider = $this->manager->getUserDefaultProvider(); 123a386a536SAndreas Gohr } 124b6119621SAndreas Gohr // remove current provider from list 125b6119621SAndreas Gohr unset($providers[$provider->getProviderID()]); 126a386a536SAndreas Gohr 127848a9be0SAndreas Gohr $form = new dokuwiki\Form\Form(['method' => 'POST']); 128848a9be0SAndreas Gohr $form->setHiddenField('do', 'twofactor_login'); 129a386a536SAndreas Gohr $form->setHiddenField('2fa_provider', $provider->getProviderID()); 130a386a536SAndreas Gohr $form->addFieldsetOpen($provider->getLabel()); 131a386a536SAndreas Gohr try { 132a386a536SAndreas Gohr $code = $provider->generateCode(); 133a386a536SAndreas Gohr $info = $provider->transmitMessage($code); 134a386a536SAndreas Gohr $form->addHTML('<p>' . hsc($info) . '</p>'); 135848a9be0SAndreas Gohr $form->addTextInput('2fa_code', 'Your Code')->val(''); 136848a9be0SAndreas Gohr $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login 137a386a536SAndreas Gohr $form->addButton('2fa', 'Submit')->attr('type', 'submit'); 138a386a536SAndreas Gohr } catch (\Exception $e) { 139a386a536SAndreas Gohr msg(hsc($e->getMessage()), -1); // FIXME better handling 140a386a536SAndreas Gohr } 141a386a536SAndreas Gohr $form->addFieldsetClose(); 142a386a536SAndreas Gohr 143a386a536SAndreas Gohr if (count($providers)) { 144a386a536SAndreas Gohr $form->addFieldsetOpen('Alternative methods'); 145a386a536SAndreas Gohr foreach ($providers as $prov) { 146*c9e42a8dSAndreas Gohr $url = wl($ID, [ 147*c9e42a8dSAndreas Gohr 'do' => 'twofactor_login', 148*c9e42a8dSAndreas Gohr '2fa_provider' => $prov->getProviderID(), 149*c9e42a8dSAndreas Gohr ]); 150*c9e42a8dSAndreas Gohr $form->addHTML('< href="' . $url . '">' . hsc($prov->getLabel()) . '</a>'); 151a386a536SAndreas Gohr } 152a386a536SAndreas Gohr $form->addFieldsetClose(); 153a386a536SAndreas Gohr } 154a386a536SAndreas Gohr 155a386a536SAndreas Gohr echo $form->toHTML(); 156a386a536SAndreas Gohr } 157a386a536SAndreas Gohr 158a386a536SAndreas Gohr /** 159a386a536SAndreas Gohr * Has the user already authenticated with the second factor? 160a386a536SAndreas Gohr * @return bool 161a386a536SAndreas Gohr */ 162a386a536SAndreas Gohr protected function isAuthed() 163a386a536SAndreas Gohr { 164848a9be0SAndreas Gohr if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 165848a9be0SAndreas Gohr $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 166848a9be0SAndreas Gohr if (!is_array($data)) return false; 1676c996db8SAndreas Gohr list($providerID, $hash,) = $data; 168848a9be0SAndreas Gohr 169848a9be0SAndreas Gohr try { 1706c996db8SAndreas Gohr $provider = $this->manager->getUserProvider($providerID); 1716c996db8SAndreas Gohr if ($this->cookieHash($provider) !== $hash) return false; 172848a9be0SAndreas Gohr return true; 173848a9be0SAndreas Gohr } catch (\Exception $e) { 174a386a536SAndreas Gohr return false; 175a386a536SAndreas Gohr } 176848a9be0SAndreas Gohr } 177a386a536SAndreas Gohr 178a386a536SAndreas Gohr /** 179a386a536SAndreas Gohr * Verify a given code 180a386a536SAndreas Gohr * 181a386a536SAndreas Gohr * @return bool 182a386a536SAndreas Gohr * @throws Exception 183a386a536SAndreas Gohr */ 184848a9be0SAndreas Gohr protected function verify($code, $providerID, $sticky) 185a386a536SAndreas Gohr { 186848a9be0SAndreas Gohr global $conf; 187848a9be0SAndreas Gohr 188a386a536SAndreas Gohr if (!$code) return false; 189a386a536SAndreas Gohr if (!$providerID) return false; 190a386a536SAndreas Gohr $provider = $this->manager->getUserProvider($providerID); 191a386a536SAndreas Gohr $ok = $provider->checkCode($code); 192848a9be0SAndreas Gohr if (!$ok) { 193848a9be0SAndreas Gohr msg('code was wrong', -1); 194848a9be0SAndreas Gohr return false; 195848a9be0SAndreas Gohr } 196a386a536SAndreas Gohr 197848a9be0SAndreas Gohr // store cookie 1986c996db8SAndreas Gohr $hash = $this->cookieHash($provider); 1996c996db8SAndreas Gohr $data = base64_encode(serialize([$providerID, $hash, time()])); 200848a9be0SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 201848a9be0SAndreas Gohr $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year 202848a9be0SAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 203a386a536SAndreas Gohr 204a386a536SAndreas Gohr return true; 205a386a536SAndreas Gohr } 2066c996db8SAndreas Gohr 2076c996db8SAndreas Gohr /** 2086c996db8SAndreas Gohr * Create a hash that validates the cookie 2096c996db8SAndreas Gohr * 2106c996db8SAndreas Gohr * @param Provider $provider 2116c996db8SAndreas Gohr * @return string 2126c996db8SAndreas Gohr */ 2136c996db8SAndreas Gohr protected function cookieHash($provider) 2146c996db8SAndreas Gohr { 2156c996db8SAndreas Gohr return sha1(join("\n", [ 2166c996db8SAndreas Gohr $provider->getProviderID(), 2176c996db8SAndreas Gohr $this->manager->getUser(), 2186c996db8SAndreas Gohr $provider->getSecret(), 2196c996db8SAndreas Gohr auth_browseruid(), 2206c996db8SAndreas Gohr auth_cookiesalt(false, true), 2216c996db8SAndreas Gohr ])); 2226c996db8SAndreas Gohr } 223fca58076SAndreas Gohr} 224