1fca58076SAndreas Gohr<?php 2fca58076SAndreas Gohr 3fca58076SAndreas Gohrnamespace dokuwiki\plugin\twofactor; 4fca58076SAndreas Gohr 55f8f561aSAndreas Gohruse dokuwiki\Extension\ActionPlugin; 6a635cb20SAndreas Gohruse dokuwiki\Form\Form; 7a635cb20SAndreas Gohruse dokuwiki\Utf8\PhpString; 8fca58076SAndreas Gohr 9fca58076SAndreas Gohr/** 10fca58076SAndreas Gohr * Baseclass for all second factor providers 11fca58076SAndreas Gohr */ 125f8f561aSAndreas Gohrabstract class Provider extends ActionPlugin 13fca58076SAndreas Gohr{ 14a635cb20SAndreas Gohr /** @var Settings */ 15a635cb20SAndreas Gohr protected $settings; 16fca58076SAndreas Gohr 17a635cb20SAndreas Gohr /** @var string */ 18a635cb20SAndreas Gohr protected $providerID; 19a635cb20SAndreas Gohr 205f8f561aSAndreas Gohr /** @inheritdoc */ 215f8f561aSAndreas Gohr public function register(\Doku_Event_Handler $controller) 225f8f561aSAndreas Gohr { 235f8f561aSAndreas Gohr $controller->register_hook( 245f8f561aSAndreas Gohr 'PLUGIN_TWOFACTOR_PROVIDER_REGISTER', 255f8f561aSAndreas Gohr 'AFTER', 265f8f561aSAndreas Gohr $this, 275f8f561aSAndreas Gohr 'registerSelf', 285f8f561aSAndreas Gohr null, 295f8f561aSAndreas Gohr Manager::EVENT_PRIORITY - 1 // providers first 305f8f561aSAndreas Gohr ); 315f8f561aSAndreas Gohr } 325f8f561aSAndreas Gohr 335f8f561aSAndreas Gohr /** 345f8f561aSAndreas Gohr * Register this class as a twofactor provider 355f8f561aSAndreas Gohr * 365f8f561aSAndreas Gohr * @param \Doku_Event $event 375f8f561aSAndreas Gohr * @return void 385f8f561aSAndreas Gohr */ 395f8f561aSAndreas Gohr public function registerSelf(\Doku_Event $event) 405f8f561aSAndreas Gohr { 415f8f561aSAndreas Gohr $event->data[$this->getProviderID()] = $this; 425f8f561aSAndreas Gohr } 435f8f561aSAndreas Gohr 44a635cb20SAndreas Gohr /** 45a635cb20SAndreas Gohr * Initializes the provider for the given user 46a635cb20SAndreas Gohr * @param string $user Current user 47a635cb20SAndreas Gohr */ 485f8f561aSAndreas Gohr public function init($user) 49a635cb20SAndreas Gohr { 505f8f561aSAndreas Gohr $this->settings = new Settings($this->getProviderID(), $user); 51a635cb20SAndreas Gohr } 52a635cb20SAndreas Gohr 5330625b49SAndreas Gohr // region Introspection methods 5430625b49SAndreas Gohr 55a635cb20SAndreas Gohr /** 56a635cb20SAndreas Gohr * The ID of this provider 57a635cb20SAndreas Gohr * 58a635cb20SAndreas Gohr * @return string 59a635cb20SAndreas Gohr */ 60a635cb20SAndreas Gohr public function getProviderID() 61a635cb20SAndreas Gohr { 625f8f561aSAndreas Gohr if (!$this->providerID) { 635f8f561aSAndreas Gohr $this->providerID = $this->getPluginName(); 645f8f561aSAndreas Gohr } 655f8f561aSAndreas Gohr 66a635cb20SAndreas Gohr return $this->providerID; 67a635cb20SAndreas Gohr } 68a635cb20SAndreas Gohr 69a635cb20SAndreas Gohr /** 70a635cb20SAndreas Gohr * Pretty Label for this provider 71a635cb20SAndreas Gohr * 72a635cb20SAndreas Gohr * @return string 73a635cb20SAndreas Gohr */ 74a635cb20SAndreas Gohr public function getLabel() 75a635cb20SAndreas Gohr { 76a635cb20SAndreas Gohr return PhpString::ucfirst($this->providerID); 77a635cb20SAndreas Gohr } 78a635cb20SAndreas Gohr 7930625b49SAndreas Gohr // endregion 8030625b49SAndreas Gohr // region Configuration methods 8130625b49SAndreas Gohr 82a635cb20SAndreas Gohr /** 83a635cb20SAndreas Gohr * Clear all settings 84a635cb20SAndreas Gohr */ 85a635cb20SAndreas Gohr public function reset() 86a635cb20SAndreas Gohr { 87a635cb20SAndreas Gohr $this->settings->purge(); 88a635cb20SAndreas Gohr } 89a635cb20SAndreas Gohr 90a635cb20SAndreas Gohr /** 91a386a536SAndreas Gohr * Has this provider been fully configured and verified by the user and thus can be used 92a635cb20SAndreas Gohr * for authentication? 93a635cb20SAndreas Gohr * 94a635cb20SAndreas Gohr * @return bool 95a635cb20SAndreas Gohr */ 96a635cb20SAndreas Gohr abstract public function isConfigured(); 97a635cb20SAndreas Gohr 98a635cb20SAndreas Gohr /** 99a635cb20SAndreas Gohr * Render the configuration form 100a635cb20SAndreas Gohr * 101a635cb20SAndreas Gohr * This method should add the needed form elements to (re)configure the provider. 102a635cb20SAndreas Gohr * The contents of the form may change depending on the current settings. 103a635cb20SAndreas Gohr * 104a635cb20SAndreas Gohr * No submit button should be added - this is handled by the main plugin. 105a635cb20SAndreas Gohr * 106a635cb20SAndreas Gohr * @param Form $form The initial form to add elements to 107a635cb20SAndreas Gohr * @return Form 108a635cb20SAndreas Gohr */ 109a635cb20SAndreas Gohr abstract public function renderProfileForm(Form $form); 110a635cb20SAndreas Gohr 111a635cb20SAndreas Gohr /** 112a635cb20SAndreas Gohr * Handle any input data 113a635cb20SAndreas Gohr * 114a635cb20SAndreas Gohr * @return void 115a635cb20SAndreas Gohr */ 116a635cb20SAndreas Gohr abstract public function handleProfileForm(); 117a635cb20SAndreas Gohr 11830625b49SAndreas Gohr // endregion 119a635cb20SAndreas Gohr // region OTP methods 120a635cb20SAndreas Gohr 121a635cb20SAndreas Gohr /** 122a635cb20SAndreas Gohr * Create and store a new secret for this provider 123a635cb20SAndreas Gohr * 124a635cb20SAndreas Gohr * @return string the new secret 125a635cb20SAndreas Gohr * @throws \Exception when no suitable random source is available 126a635cb20SAndreas Gohr */ 127a635cb20SAndreas Gohr public function initSecret() 128a635cb20SAndreas Gohr { 129a635cb20SAndreas Gohr $ga = new GoogleAuthenticator(); 130a635cb20SAndreas Gohr $secret = $ga->createSecret(); 131a635cb20SAndreas Gohr 132a635cb20SAndreas Gohr $this->settings->set('secret', $secret); 133a635cb20SAndreas Gohr return $secret; 134a635cb20SAndreas Gohr } 135a635cb20SAndreas Gohr 136a635cb20SAndreas Gohr /** 1376c996db8SAndreas Gohr * Get the secret for this provider 1386c996db8SAndreas Gohr * 1396c996db8SAndreas Gohr * @return string 1406c996db8SAndreas Gohr */ 1416c996db8SAndreas Gohr public function getSecret() 1426c996db8SAndreas Gohr { 1436c996db8SAndreas Gohr return $this->settings->get('secret'); 1446c996db8SAndreas Gohr } 1456c996db8SAndreas Gohr 1466c996db8SAndreas Gohr /** 147a635cb20SAndreas Gohr * Generate an auth code 148a635cb20SAndreas Gohr * 149a635cb20SAndreas Gohr * @return string 150a635cb20SAndreas Gohr * @throws \Exception when no code can be created 151a635cb20SAndreas Gohr */ 152a635cb20SAndreas Gohr public function generateCode() 153a635cb20SAndreas Gohr { 154a635cb20SAndreas Gohr $secret = $this->settings->get('secret'); 155a635cb20SAndreas Gohr if (!$secret) throw new \Exception('No secret for provider ' . $this->getProviderID()); 156a635cb20SAndreas Gohr 157a635cb20SAndreas Gohr $ga = new GoogleAuthenticator(); 158a635cb20SAndreas Gohr return $ga->getCode($secret); 159a635cb20SAndreas Gohr } 160a635cb20SAndreas Gohr 161a635cb20SAndreas Gohr /** 162a635cb20SAndreas Gohr * Check the given code 163a635cb20SAndreas Gohr * 164a635cb20SAndreas Gohr * @param string $code 165*16ed3964SAndreas Gohr * @param bool $usermessage should a message about the failed code be shown to the user? 166a386a536SAndreas Gohr * @return bool 167*16ed3964SAndreas Gohr * @throws \RuntimeException when no code can be created 168a635cb20SAndreas Gohr */ 169*16ed3964SAndreas Gohr public function checkCode($code, $usermessage = true) 170a635cb20SAndreas Gohr { 171a635cb20SAndreas Gohr $secret = $this->settings->get('secret'); 172*16ed3964SAndreas Gohr if (!$secret) throw new \RuntimeException('No secret for provider ' . $this->getProviderID()); 173a635cb20SAndreas Gohr 174a635cb20SAndreas Gohr $ga = new GoogleAuthenticator(); 175*16ed3964SAndreas Gohr $ok = $ga->verifyCode($secret, $code, $this->getTolerance()); 176*16ed3964SAndreas Gohr if (!$ok && $usermessage) { 177*16ed3964SAndreas Gohr msg((Manager::getInstance())->getLang('codefail'), -1); 178*16ed3964SAndreas Gohr } 179*16ed3964SAndreas Gohr return $ok; 1802fadf188SAndreas Gohr } 1812fadf188SAndreas Gohr 1822fadf188SAndreas Gohr /** 1832fadf188SAndreas Gohr * The tolerance to be used when verifying codes 1842fadf188SAndreas Gohr * 1852fadf188SAndreas Gohr * This is the allowed time drift in 30 second units (8 means 4 minutes before or after) 1862fadf188SAndreas Gohr * Different providers may want to use different tolerances by overriding this method. 1872fadf188SAndreas Gohr * 1882fadf188SAndreas Gohr * @return int 1892fadf188SAndreas Gohr */ 1902fadf188SAndreas Gohr public function getTolerance() 1912fadf188SAndreas Gohr { 1922fadf188SAndreas Gohr return 2; 193a635cb20SAndreas Gohr } 194a635cb20SAndreas Gohr 195a635cb20SAndreas Gohr /** 19630625b49SAndreas Gohr * Transmits the code to the user 19730625b49SAndreas Gohr * 19830625b49SAndreas Gohr * If a provider does not transmit anything (eg. TOTP) simply 19930625b49SAndreas Gohr * return the message. 20030625b49SAndreas Gohr * 20130625b49SAndreas Gohr * @param string $code The code to transmit 20230625b49SAndreas Gohr * @return string Informational message for the user 20330625b49SAndreas Gohr * @throw \Exception when the message can't be sent 204a635cb20SAndreas Gohr */ 20530625b49SAndreas Gohr abstract public function transmitMessage($code); 206a635cb20SAndreas Gohr 207a635cb20SAndreas Gohr // endregion 208fca58076SAndreas Gohr} 209