1fca58076SAndreas Gohr<?php 2fca58076SAndreas Gohr 3fca58076SAndreas Gohrnamespace dokuwiki\plugin\twofactor; 4fca58076SAndreas Gohr 55f8f561aSAndreas Gohruse dokuwiki\Extension\ActionPlugin; 6*c8525a21SAndreas Gohruse dokuwiki\Extension\AuthPlugin; 7a635cb20SAndreas Gohruse dokuwiki\Form\Form; 8a635cb20SAndreas Gohruse dokuwiki\Utf8\PhpString; 9fca58076SAndreas Gohr 10fca58076SAndreas Gohr/** 11fca58076SAndreas Gohr * Baseclass for all second factor providers 12fca58076SAndreas Gohr */ 135f8f561aSAndreas Gohrabstract class Provider extends ActionPlugin 14fca58076SAndreas Gohr{ 15a635cb20SAndreas Gohr /** @var Settings */ 16a635cb20SAndreas Gohr protected $settings; 17fca58076SAndreas Gohr 18a635cb20SAndreas Gohr /** @var string */ 19a635cb20SAndreas Gohr protected $providerID; 20a635cb20SAndreas Gohr 215f8f561aSAndreas Gohr /** @inheritdoc */ 225f8f561aSAndreas Gohr public function register(\Doku_Event_Handler $controller) 235f8f561aSAndreas Gohr { 245f8f561aSAndreas Gohr $controller->register_hook( 255f8f561aSAndreas Gohr 'PLUGIN_TWOFACTOR_PROVIDER_REGISTER', 265f8f561aSAndreas Gohr 'AFTER', 275f8f561aSAndreas Gohr $this, 285f8f561aSAndreas Gohr 'registerSelf', 295f8f561aSAndreas Gohr null, 305f8f561aSAndreas Gohr Manager::EVENT_PRIORITY - 1 // providers first 315f8f561aSAndreas Gohr ); 325f8f561aSAndreas Gohr } 335f8f561aSAndreas Gohr 345f8f561aSAndreas Gohr /** 355f8f561aSAndreas Gohr * Register this class as a twofactor provider 365f8f561aSAndreas Gohr * 375f8f561aSAndreas Gohr * @param \Doku_Event $event 385f8f561aSAndreas Gohr * @return void 395f8f561aSAndreas Gohr */ 405f8f561aSAndreas Gohr public function registerSelf(\Doku_Event $event) 415f8f561aSAndreas Gohr { 425f8f561aSAndreas Gohr $event->data[$this->getProviderID()] = $this; 435f8f561aSAndreas Gohr } 445f8f561aSAndreas Gohr 45a635cb20SAndreas Gohr /** 46a635cb20SAndreas Gohr * Initializes the provider for the given user 47a635cb20SAndreas Gohr * @param string $user Current user 48a635cb20SAndreas Gohr */ 495f8f561aSAndreas Gohr public function init($user) 50a635cb20SAndreas Gohr { 515f8f561aSAndreas Gohr $this->settings = new Settings($this->getProviderID(), $user); 52a635cb20SAndreas Gohr } 53a635cb20SAndreas Gohr 5430625b49SAndreas Gohr // region Introspection methods 5530625b49SAndreas Gohr 56a635cb20SAndreas Gohr /** 57*c8525a21SAndreas Gohr * The user data for the current user 58*c8525a21SAndreas Gohr * @return array (user=>'login', name=>'full name', mail=>'user@example.com', grps=>['group1', 'group2',...]) 59*c8525a21SAndreas Gohr */ 60*c8525a21SAndreas Gohr public function getUserData() 61*c8525a21SAndreas Gohr { 62*c8525a21SAndreas Gohr /** @var AuthPlugin $auth */ 63*c8525a21SAndreas Gohr global $auth; 64*c8525a21SAndreas Gohr $user = $this->settings->getUser(); 65*c8525a21SAndreas Gohr $userdata = $auth->getUserData($user); 66*c8525a21SAndreas Gohr if (!$userdata) throw new \RuntimeException('2fa: Failed to get user details from auth backend'); 67*c8525a21SAndreas Gohr $userdata['user'] = $user; 68*c8525a21SAndreas Gohr return $userdata; 69*c8525a21SAndreas Gohr } 70*c8525a21SAndreas Gohr 71*c8525a21SAndreas Gohr /** 72a635cb20SAndreas Gohr * The ID of this provider 73a635cb20SAndreas Gohr * 74a635cb20SAndreas Gohr * @return string 75a635cb20SAndreas Gohr */ 76a635cb20SAndreas Gohr public function getProviderID() 77a635cb20SAndreas Gohr { 785f8f561aSAndreas Gohr if (!$this->providerID) { 795f8f561aSAndreas Gohr $this->providerID = $this->getPluginName(); 805f8f561aSAndreas Gohr } 815f8f561aSAndreas Gohr 82a635cb20SAndreas Gohr return $this->providerID; 83a635cb20SAndreas Gohr } 84a635cb20SAndreas Gohr 85a635cb20SAndreas Gohr /** 86a635cb20SAndreas Gohr * Pretty Label for this provider 87a635cb20SAndreas Gohr * 88a635cb20SAndreas Gohr * @return string 89a635cb20SAndreas Gohr */ 90a635cb20SAndreas Gohr public function getLabel() 91a635cb20SAndreas Gohr { 92a635cb20SAndreas Gohr return PhpString::ucfirst($this->providerID); 93a635cb20SAndreas Gohr } 94a635cb20SAndreas Gohr 9530625b49SAndreas Gohr // endregion 9630625b49SAndreas Gohr // region Configuration methods 9730625b49SAndreas Gohr 98a635cb20SAndreas Gohr /** 99a635cb20SAndreas Gohr * Clear all settings 100a635cb20SAndreas Gohr */ 101a635cb20SAndreas Gohr public function reset() 102a635cb20SAndreas Gohr { 103a635cb20SAndreas Gohr $this->settings->purge(); 104a635cb20SAndreas Gohr } 105a635cb20SAndreas Gohr 106a635cb20SAndreas Gohr /** 107a386a536SAndreas Gohr * Has this provider been fully configured and verified by the user and thus can be used 108a635cb20SAndreas Gohr * for authentication? 109a635cb20SAndreas Gohr * 110a635cb20SAndreas Gohr * @return bool 111a635cb20SAndreas Gohr */ 112a635cb20SAndreas Gohr abstract public function isConfigured(); 113a635cb20SAndreas Gohr 114a635cb20SAndreas Gohr /** 115a635cb20SAndreas Gohr * Render the configuration form 116a635cb20SAndreas Gohr * 117a635cb20SAndreas Gohr * This method should add the needed form elements to (re)configure the provider. 118a635cb20SAndreas Gohr * The contents of the form may change depending on the current settings. 119a635cb20SAndreas Gohr * 120a635cb20SAndreas Gohr * No submit button should be added - this is handled by the main plugin. 121a635cb20SAndreas Gohr * 122a635cb20SAndreas Gohr * @param Form $form The initial form to add elements to 123a635cb20SAndreas Gohr * @return Form 124a635cb20SAndreas Gohr */ 125a635cb20SAndreas Gohr abstract public function renderProfileForm(Form $form); 126a635cb20SAndreas Gohr 127a635cb20SAndreas Gohr /** 128a635cb20SAndreas Gohr * Handle any input data 129a635cb20SAndreas Gohr * 130a635cb20SAndreas Gohr * @return void 131a635cb20SAndreas Gohr */ 132a635cb20SAndreas Gohr abstract public function handleProfileForm(); 133a635cb20SAndreas Gohr 13430625b49SAndreas Gohr // endregion 135a635cb20SAndreas Gohr // region OTP methods 136a635cb20SAndreas Gohr 137a635cb20SAndreas Gohr /** 138a635cb20SAndreas Gohr * Create and store a new secret for this provider 139a635cb20SAndreas Gohr * 140a635cb20SAndreas Gohr * @return string the new secret 141a635cb20SAndreas Gohr * @throws \Exception when no suitable random source is available 142a635cb20SAndreas Gohr */ 143a635cb20SAndreas Gohr public function initSecret() 144a635cb20SAndreas Gohr { 145a635cb20SAndreas Gohr $ga = new GoogleAuthenticator(); 146a635cb20SAndreas Gohr $secret = $ga->createSecret(); 147a635cb20SAndreas Gohr 148a635cb20SAndreas Gohr $this->settings->set('secret', $secret); 149a635cb20SAndreas Gohr return $secret; 150a635cb20SAndreas Gohr } 151a635cb20SAndreas Gohr 152a635cb20SAndreas Gohr /** 1536c996db8SAndreas Gohr * Get the secret for this provider 1546c996db8SAndreas Gohr * 1556c996db8SAndreas Gohr * @return string 1566c996db8SAndreas Gohr */ 1576c996db8SAndreas Gohr public function getSecret() 1586c996db8SAndreas Gohr { 1596c996db8SAndreas Gohr return $this->settings->get('secret'); 1606c996db8SAndreas Gohr } 1616c996db8SAndreas Gohr 1626c996db8SAndreas Gohr /** 163a635cb20SAndreas Gohr * Generate an auth code 164a635cb20SAndreas Gohr * 165a635cb20SAndreas Gohr * @return string 166a635cb20SAndreas Gohr * @throws \Exception when no code can be created 167a635cb20SAndreas Gohr */ 168a635cb20SAndreas Gohr public function generateCode() 169a635cb20SAndreas Gohr { 170a635cb20SAndreas Gohr $secret = $this->settings->get('secret'); 171a635cb20SAndreas Gohr if (!$secret) throw new \Exception('No secret for provider ' . $this->getProviderID()); 172a635cb20SAndreas Gohr 173a635cb20SAndreas Gohr $ga = new GoogleAuthenticator(); 174a635cb20SAndreas Gohr return $ga->getCode($secret); 175a635cb20SAndreas Gohr } 176a635cb20SAndreas Gohr 177a635cb20SAndreas Gohr /** 178a635cb20SAndreas Gohr * Check the given code 179a635cb20SAndreas Gohr * 180a635cb20SAndreas Gohr * @param string $code 18116ed3964SAndreas Gohr * @param bool $usermessage should a message about the failed code be shown to the user? 182a386a536SAndreas Gohr * @return bool 18316ed3964SAndreas Gohr * @throws \RuntimeException when no code can be created 184a635cb20SAndreas Gohr */ 18516ed3964SAndreas Gohr public function checkCode($code, $usermessage = true) 186a635cb20SAndreas Gohr { 187a635cb20SAndreas Gohr $secret = $this->settings->get('secret'); 18816ed3964SAndreas Gohr if (!$secret) throw new \RuntimeException('No secret for provider ' . $this->getProviderID()); 189a635cb20SAndreas Gohr 190a635cb20SAndreas Gohr $ga = new GoogleAuthenticator(); 19116ed3964SAndreas Gohr $ok = $ga->verifyCode($secret, $code, $this->getTolerance()); 19216ed3964SAndreas Gohr if (!$ok && $usermessage) { 19316ed3964SAndreas Gohr msg((Manager::getInstance())->getLang('codefail'), -1); 19416ed3964SAndreas Gohr } 19516ed3964SAndreas Gohr return $ok; 1962fadf188SAndreas Gohr } 1972fadf188SAndreas Gohr 1982fadf188SAndreas Gohr /** 1992fadf188SAndreas Gohr * The tolerance to be used when verifying codes 2002fadf188SAndreas Gohr * 2012fadf188SAndreas Gohr * This is the allowed time drift in 30 second units (8 means 4 minutes before or after) 2022fadf188SAndreas Gohr * Different providers may want to use different tolerances by overriding this method. 2032fadf188SAndreas Gohr * 2042fadf188SAndreas Gohr * @return int 2052fadf188SAndreas Gohr */ 2062fadf188SAndreas Gohr public function getTolerance() 2072fadf188SAndreas Gohr { 2082fadf188SAndreas Gohr return 2; 209a635cb20SAndreas Gohr } 210a635cb20SAndreas Gohr 211a635cb20SAndreas Gohr /** 21230625b49SAndreas Gohr * Transmits the code to the user 21330625b49SAndreas Gohr * 21430625b49SAndreas Gohr * If a provider does not transmit anything (eg. TOTP) simply 21530625b49SAndreas Gohr * return the message. 21630625b49SAndreas Gohr * 21730625b49SAndreas Gohr * @param string $code The code to transmit 21830625b49SAndreas Gohr * @return string Informational message for the user 21930625b49SAndreas Gohr * @throw \Exception when the message can't be sent 220a635cb20SAndreas Gohr */ 22130625b49SAndreas Gohr abstract public function transmitMessage($code); 222a635cb20SAndreas Gohr 223a635cb20SAndreas Gohr // endregion 224fca58076SAndreas Gohr} 225