1<?php 2 3namespace dokuwiki\plugin\twofactor; 4 5use dokuwiki\Extension\ActionPlugin; 6use dokuwiki\Form\Form; 7use dokuwiki\Utf8\PhpString; 8 9/** 10 * Baseclass for all second factor providers 11 */ 12abstract class Provider extends ActionPlugin 13{ 14 /** @var Settings */ 15 protected $settings; 16 17 /** @var string */ 18 protected $providerID; 19 20 /** @inheritdoc */ 21 public function register(\Doku_Event_Handler $controller) 22 { 23 $controller->register_hook( 24 'PLUGIN_TWOFACTOR_PROVIDER_REGISTER', 25 'AFTER', 26 $this, 27 'registerSelf', 28 null, 29 Manager::EVENT_PRIORITY - 1 // providers first 30 ); 31 } 32 33 /** 34 * Register this class as a twofactor provider 35 * 36 * @param \Doku_Event $event 37 * @return void 38 */ 39 public function registerSelf(\Doku_Event $event) 40 { 41 $event->data[$this->getProviderID()] = $this; 42 } 43 44 /** 45 * Initializes the provider for the given user 46 * @param string $user Current user 47 */ 48 public function init($user) 49 { 50 $this->settings = new Settings($this->getProviderID(), $user); 51 } 52 53 // region Introspection methods 54 55 /** 56 * The ID of this provider 57 * 58 * @return string 59 */ 60 public function getProviderID() 61 { 62 if (!$this->providerID) { 63 $this->providerID = $this->getPluginName(); 64 } 65 66 return $this->providerID; 67 } 68 69 /** 70 * Pretty Label for this provider 71 * 72 * @return string 73 */ 74 public function getLabel() 75 { 76 return PhpString::ucfirst($this->providerID); 77 } 78 79 // endregion 80 // region Configuration methods 81 82 /** 83 * Clear all settings 84 */ 85 public function reset() 86 { 87 $this->settings->purge(); 88 } 89 90 /** 91 * Has this provider been fully configured and verified by the user and thus can be used 92 * for authentication? 93 * 94 * @return bool 95 */ 96 abstract public function isConfigured(); 97 98 /** 99 * Render the configuration form 100 * 101 * This method should add the needed form elements to (re)configure the provider. 102 * The contents of the form may change depending on the current settings. 103 * 104 * No submit button should be added - this is handled by the main plugin. 105 * 106 * @param Form $form The initial form to add elements to 107 * @return Form 108 */ 109 abstract public function renderProfileForm(Form $form); 110 111 /** 112 * Handle any input data 113 * 114 * @return void 115 */ 116 abstract public function handleProfileForm(); 117 118 // endregion 119 // region OTP methods 120 121 /** 122 * Create and store a new secret for this provider 123 * 124 * @return string the new secret 125 * @throws \Exception when no suitable random source is available 126 */ 127 public function initSecret() 128 { 129 $ga = new GoogleAuthenticator(); 130 $secret = $ga->createSecret(); 131 132 $this->settings->set('secret', $secret); 133 return $secret; 134 } 135 136 /** 137 * Get the secret for this provider 138 * 139 * @return string 140 */ 141 public function getSecret() 142 { 143 return $this->settings->get('secret'); 144 } 145 146 /** 147 * Generate an auth code 148 * 149 * @return string 150 * @throws \Exception when no code can be created 151 */ 152 public function generateCode() 153 { 154 $secret = $this->settings->get('secret'); 155 if (!$secret) throw new \Exception('No secret for provider ' . $this->getProviderID()); 156 157 $ga = new GoogleAuthenticator(); 158 return $ga->getCode($secret); 159 } 160 161 /** 162 * Check the given code 163 * 164 * @param string $code 165 * @param bool $usermessage should a message about the failed code be shown to the user? 166 * @return bool 167 * @throws \RuntimeException when no code can be created 168 */ 169 public function checkCode($code, $usermessage = true) 170 { 171 $secret = $this->settings->get('secret'); 172 if (!$secret) throw new \RuntimeException('No secret for provider ' . $this->getProviderID()); 173 174 $ga = new GoogleAuthenticator(); 175 $ok = $ga->verifyCode($secret, $code, $this->getTolerance()); 176 if (!$ok && $usermessage) { 177 msg((Manager::getInstance())->getLang('codefail'), -1); 178 } 179 return $ok; 180 } 181 182 /** 183 * The tolerance to be used when verifying codes 184 * 185 * This is the allowed time drift in 30 second units (8 means 4 minutes before or after) 186 * Different providers may want to use different tolerances by overriding this method. 187 * 188 * @return int 189 */ 190 public function getTolerance() 191 { 192 return 2; 193 } 194 195 /** 196 * Transmits the code to the user 197 * 198 * If a provider does not transmit anything (eg. TOTP) simply 199 * return the message. 200 * 201 * @param string $code The code to transmit 202 * @return string Informational message for the user 203 * @throw \Exception when the message can't be sent 204 */ 205 abstract public function transmitMessage($code); 206 207 // endregion 208} 209