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