1<?php 2 3namespace dokuwiki\plugin\oauth; 4 5use dokuwiki\Logger; 6 7/** 8 * Implements the flow control for oAuth 9 */ 10class OAuthManager 11{ 12 // region main flow 13 14 /** 15 * Explicitly starts the oauth flow by redirecting to IDP 16 * 17 * @throws \OAuth\Common\Exception\Exception 18 */ 19 public function startFlow($servicename) 20 { 21 global $ID; 22 23 $session = Session::getInstance(); 24 $session->setLoginData($servicename, $ID); 25 26 $service = $this->loadService($servicename); 27 $service->initOAuthService(); 28 $service->login(); // redirects 29 } 30 31 /** 32 * Continues the flow from various states 33 * 34 * @return bool true if the login has been handled 35 * @throws Exception 36 * @throws \OAuth\Common\Exception\Exception 37 */ 38 public function continueFlow() 39 { 40 return $this->loginByService() || $this->loginBySession() || $this->loginByCookie(); 41 } 42 43 /** 44 * Second step in a explicit login, validates the oauth code 45 * 46 * @return bool true if successful, false if not applies 47 * @throws \OAuth\Common\Exception\Exception 48 */ 49 protected function loginByService() 50 { 51 global $INPUT; 52 53 if (!$INPUT->get->has('code') && !$INPUT->get->has('oauth_token')) { 54 return false; 55 } 56 57 $session = Session::getInstance(); 58 59 // init service from session 60 $logindata = $session->getLoginData(); 61 if (!$logindata) return false; 62 $service = $this->loadService($logindata['servicename']); 63 $service->initOAuthService(); 64 65 $session->clearLoginData(); 66 67 // oAuth login 68 if (!$service->checkToken()) throw new \OAuth\Common\Exception\Exception("Invalid Token - Login failed"); 69 $userdata = $service->getUser(); 70 71 // processing 72 $userdata = $this->validateUserData($userdata, $logindata['servicename']); 73 $userdata = $this->processUserData($userdata, $logindata['servicename']); 74 75 // store data 76 $storageId = $this->getStorageId($userdata['mail']); 77 $service->upgradeStorage($storageId); 78 79 // login 80 $session->setUser($userdata); // log in 81 $session->setCookie($logindata['servicename'], $storageId); // set cookie 82 83 // redirect to the appropriate ID 84 if (!empty($logindata['id'])) { 85 send_redirect(wl($logindata['id'], [], true, '&')); 86 } 87 return true; 88 } 89 90 /** 91 * Login based on user's current session data 92 * 93 * This will also log in plainauth users 94 * 95 * @return bool true if successful, false if not applies 96 * @throws Exception 97 */ 98 protected function loginBySession() 99 { 100 $session = Session::getInstance(); 101 if (!$session->isValid()) { 102 $session->clear(); 103 return false; 104 } 105 106 $userdata = $session->getUser(); 107 if (!$userdata) return false; 108 if (!isset($userdata['user'])) return false; // default dokuwiki does not put username here, let DW handle it 109 $session->setUser($userdata, false); // does a login without resetting the time 110 return true; 111 } 112 113 /** 114 * Login based on user cookie and a previously saved access token 115 * 116 * @return bool true if successful, false if not applies 117 * @throws \OAuth\Common\Exception\Exception 118 */ 119 protected function loginByCookie() 120 { 121 $session = Session::getInstance(); 122 $cookie = $session->getCookie(); 123 if (!$cookie) return false; 124 125 $service = $this->loadService($cookie['servicename']); 126 $service->initOAuthService($cookie['storageId']); 127 128 // ensure that we have a current access token 129 $service->refreshOutdatedToken(); 130 131 // this should use a previously saved token 132 $userdata = $service->getUser(); 133 134 // processing 135 $userdata = $this->validateUserData($userdata, $cookie['servicename']); 136 $userdata = $this->processUserData($userdata, $cookie['servicename']); 137 138 $session->setUser($userdata); // log in 139 return true; 140 } 141 142 /** 143 * Callback service's logout 144 * 145 * @return void 146 */ 147 public function logout() 148 { 149 $session = Session::getInstance(); 150 $cookie = $session->getCookie(); 151 if (!$cookie) return; 152 try { 153 $service = $this->loadService($cookie['servicename']); 154 $service->initOAuthService($cookie['storageId']); 155 $service->logout(); 156 } catch (\OAuth\Common\Exception\Exception $e) { 157 return; 158 } 159 } 160 161 // endregion 162 163 /** 164 * The ID we store authentication data as 165 * 166 * @param string $mail 167 * @return string 168 */ 169 protected function getStorageId($mail) 170 { 171 return md5($mail); 172 } 173 174 /** 175 * Clean and validate the user data provided from the service 176 * 177 * @param array $userdata 178 * @param string $servicename 179 * @return array 180 * @throws Exception 181 */ 182 protected function validateUserData($userdata, $servicename) 183 { 184 /** @var \auth_plugin_oauth */ 185 global $auth; 186 187 // mail is required 188 if (empty($userdata['mail'])) { 189 throw new Exception('noEmail', [$servicename]); 190 } 191 192 $userdata['mail'] = strtolower($userdata['mail']); 193 194 // mail needs to be allowed 195 /** @var \helper_plugin_oauth $hlp */ 196 $hlp = plugin_load('helper', 'oauth'); 197 198 if (!$hlp->checkMail($userdata['mail'])) { 199 throw new Exception('rejectedEMail', [implode(', ', $hlp->getValidDomains())]); 200 } 201 202 // make username from mail if empty 203 if (!isset($userdata['user'])) $userdata['user'] = ''; 204 $userdata['user'] = $auth->cleanUser((string)$userdata['user']); 205 if ($userdata['user'] === '') { 206 [$userdata['user']] = explode('@', $userdata['mail']); 207 } 208 209 // make full name from username if empty 210 if (empty($userdata['name'])) { 211 $userdata['name'] = $userdata['user']; 212 } 213 214 // make sure groups are array and valid 215 if (!isset($userdata['grps'])) $userdata['grps'] = []; 216 $userdata['grps'] = array_map([$auth, 'cleanGroup'], (array)$userdata['grps']); 217 218 return $userdata; 219 } 220 221 /** 222 * Process the userdata, update the user info array and create the user if necessary 223 * 224 * Uses the global $auth object for user management 225 * 226 * @param array $userdata User info received from authentication 227 * @param string $servicename Auth service 228 * @return array the modified user info 229 * @throws Exception 230 */ 231 protected function processUserData($userdata, $servicename) 232 { 233 /** @var \auth_plugin_oauth $auth */ 234 global $auth; 235 236 // see if the user is known already 237 $localUser = $auth->getUserByEmail($userdata['mail']); 238 if ($localUser) { 239 $localUserInfo = $auth->getUserData($localUser); 240 $localUserInfo['user'] = $localUser; 241 if (isset($localUserInfo['pass'])) unset($localUserInfo['pass']); 242 243 // check if the user allowed access via this service 244 if (!in_array($auth->cleanGroup($servicename), $localUserInfo['grps'])) { 245 throw new Exception('authnotenabled', [$servicename]); 246 } 247 248 $helper = plugin_load('helper', 'oauth'); 249 250 $userdata['user'] = $localUser; 251 $userdata['name'] = $localUserInfo['name']; 252 $userdata['grps'] = $this->mergeGroups( 253 $localUserInfo['grps'], 254 $userdata['grps'] ?? [], 255 array_keys($helper->listServices(false)), 256 $auth->getConf('overwrite-groups') 257 ); 258 259 // update user if changed 260 sort($localUserInfo['grps']); 261 sort($userdata['grps']); 262 if ($localUserInfo != $userdata && !isset($localUserInfo['protected'])) { 263 $auth->modifyUser($localUser, $userdata); 264 } 265 } elseif (actionOK('register') || $auth->getConf('register-on-auth')) { 266 if (!$auth->registerOAuthUser($userdata, $servicename)) { 267 throw new Exception('generic create error'); 268 } 269 } else { 270 throw new Exception('addUser not possible'); 271 } 272 273 return $userdata; 274 } 275 276 /** 277 * Merges local and provider user groups. Keeps internal 278 * Dokuwiki groups unless configured to overwrite all ('overwrite-groups' setting) 279 * 280 * @param string[] $localGroups Local user groups 281 * @param string[] $providerGroups Groups fetched from the provider 282 * @param string[] $servicenames Service names that should be kept if set 283 * @param bool $overwrite Config setting to overwrite local DokuWiki groups 284 * 285 * @return array 286 */ 287 protected function mergeGroups($localGroups, $providerGroups, $servicenames, $overwrite) 288 { 289 global $conf; 290 291 // overwrite-groups set in config - remove all local groups except services and default 292 if ($overwrite) { 293 $localGroups = array_intersect($localGroups, array_merge($servicenames, [$conf['defaultgroup']])); 294 } 295 296 return array_unique(array_merge($localGroups, $providerGroups)); 297 } 298 299 /** 300 * Instantiates a Service by name 301 * 302 * @param string $servicename 303 * @return Adapter 304 * @throws Exception 305 */ 306 protected function loadService($servicename) 307 { 308 /** @var \helper_plugin_oauth $hlp */ 309 $hlp = plugin_load('helper', 'oauth'); 310 $srv = $hlp->loadService($servicename); 311 312 if ($srv === null) throw new Exception("No such service $servicename"); 313 return $srv; 314 } 315} 316