174b4d4a4SAndreas Gohr<?php 274b4d4a4SAndreas Gohr 374b4d4a4SAndreas Gohrnamespace dokuwiki\plugin\oauth; 474b4d4a4SAndreas Gohr 5*bfebac18SAndreas Gohruse dokuwiki\Logger; 6*bfebac18SAndreas Gohr 76d9a8a49SAndreas Gohr/** 86d9a8a49SAndreas Gohr * Implements the flow control for oAuth 96d9a8a49SAndreas Gohr */ 1074b4d4a4SAndreas Gohrclass OAuthManager 1174b4d4a4SAndreas Gohr{ 126d9a8a49SAndreas Gohr // region main flow 1374b4d4a4SAndreas Gohr 1474b4d4a4SAndreas Gohr /** 1531039e80SAndreas Gohr * Explicitly starts the oauth flow by redirecting to IDP 1631039e80SAndreas Gohr * 1704a78b87SAndreas Gohr * @throws \OAuth\Common\Exception\Exception 1874b4d4a4SAndreas Gohr */ 1974b4d4a4SAndreas Gohr public function startFlow($servicename) 2074b4d4a4SAndreas Gohr { 2131039e80SAndreas Gohr global $ID; 2231039e80SAndreas Gohr 2374b4d4a4SAndreas Gohr $session = Session::getInstance(); 2428002081SAndreas Gohr $session->setLoginData($servicename, $ID); 2574b4d4a4SAndreas Gohr $service = $this->loadService($servicename); 2628002081SAndreas Gohr $service->initOAuthService(); 2774b4d4a4SAndreas Gohr $service->login(); // redirects 2874b4d4a4SAndreas Gohr } 2974b4d4a4SAndreas Gohr 3074b4d4a4SAndreas Gohr /** 3131039e80SAndreas Gohr * Continues the flow from various states 3231039e80SAndreas Gohr * 3374b4d4a4SAndreas Gohr * @return bool true if the login has been handled 3474b4d4a4SAndreas Gohr * @throws Exception 3574b4d4a4SAndreas Gohr * @throws \OAuth\Common\Exception\Exception 3674b4d4a4SAndreas Gohr */ 3774b4d4a4SAndreas Gohr public function continueFlow() 3874b4d4a4SAndreas Gohr { 3974b4d4a4SAndreas Gohr return $this->loginByService() or 4074b4d4a4SAndreas Gohr $this->loginBySession() or 4174b4d4a4SAndreas Gohr $this->loginByCookie(); 4274b4d4a4SAndreas Gohr } 4374b4d4a4SAndreas Gohr 4474b4d4a4SAndreas Gohr /** 456d9a8a49SAndreas Gohr * Second step in a explicit login, validates the oauth code 466d9a8a49SAndreas Gohr * 476d9a8a49SAndreas Gohr * @return bool true if successful, false if not applies 4874b4d4a4SAndreas Gohr * @throws \OAuth\Common\Exception\Exception 4974b4d4a4SAndreas Gohr */ 5074b4d4a4SAndreas Gohr protected function loginByService() 5174b4d4a4SAndreas Gohr { 5274b4d4a4SAndreas Gohr global $INPUT; 5374b4d4a4SAndreas Gohr 5474b4d4a4SAndreas Gohr if (!$INPUT->get->has('code') && !$INPUT->get->has('oauth_token')) { 5574b4d4a4SAndreas Gohr return false; 5674b4d4a4SAndreas Gohr } 5774b4d4a4SAndreas Gohr 5874b4d4a4SAndreas Gohr $session = Session::getInstance(); 5974b4d4a4SAndreas Gohr 6074b4d4a4SAndreas Gohr // init service from session 6174b4d4a4SAndreas Gohr $logindata = $session->getLoginData(); 6274b4d4a4SAndreas Gohr if (!$logindata) return false; 6374b4d4a4SAndreas Gohr $service = $this->loadService($logindata['servicename']); 6428002081SAndreas Gohr $service->initOAuthService(); 6574b4d4a4SAndreas Gohr $session->clearLoginData(); 6674b4d4a4SAndreas Gohr 6774b4d4a4SAndreas Gohr // oAuth login 6804a78b87SAndreas Gohr if (!$service->checkToken()) throw new \OAuth\Common\Exception\Exception("Invalid Token - Login failed"); 6974b4d4a4SAndreas Gohr $userdata = $service->getUser(); 7074b4d4a4SAndreas Gohr 7174b4d4a4SAndreas Gohr // processing 7274b4d4a4SAndreas Gohr $userdata = $this->validateUserData($userdata, $logindata['servicename']); 7374b4d4a4SAndreas Gohr $userdata = $this->processUserData($userdata, $logindata['servicename']); 7474b4d4a4SAndreas Gohr 7528002081SAndreas Gohr // store data 7628002081SAndreas Gohr $storageId = $this->getStorageId($userdata['mail']); 7728002081SAndreas Gohr $service->upgradeStorage($storageId); 7828002081SAndreas Gohr 7974b4d4a4SAndreas Gohr // login 8074b4d4a4SAndreas Gohr $session->setUser($userdata); // log in 8128002081SAndreas Gohr $session->setCookie($logindata['servicename'], $storageId); // set cookie 8274b4d4a4SAndreas Gohr 8331039e80SAndreas Gohr // redirect to the appropriate ID 8431039e80SAndreas Gohr if (!empty($logindata['id'])) { 8531039e80SAndreas Gohr send_redirect(wl($logindata['id'], [], true, '&')); 8631039e80SAndreas Gohr } 8774b4d4a4SAndreas Gohr return true; 8874b4d4a4SAndreas Gohr } 8974b4d4a4SAndreas Gohr 9074b4d4a4SAndreas Gohr /** 916d9a8a49SAndreas Gohr * Login based on user's current session data 9274b4d4a4SAndreas Gohr * 9374b4d4a4SAndreas Gohr * This will also log in plainauth users 9474b4d4a4SAndreas Gohr * 956d9a8a49SAndreas Gohr * @return bool true if successful, false if not applies 9674b4d4a4SAndreas Gohr * @throws Exception 9774b4d4a4SAndreas Gohr */ 9874b4d4a4SAndreas Gohr protected function loginBySession() 9974b4d4a4SAndreas Gohr { 10074b4d4a4SAndreas Gohr $session = Session::getInstance(); 10174b4d4a4SAndreas Gohr if (!$session->isValid()) { 10274b4d4a4SAndreas Gohr $session->clear(); 10374b4d4a4SAndreas Gohr return false; 10474b4d4a4SAndreas Gohr } 10574b4d4a4SAndreas Gohr 10674b4d4a4SAndreas Gohr $userdata = $session->getUser(); 10731039e80SAndreas Gohr if (!$userdata) return false; 10804a78b87SAndreas Gohr if (!isset($userdata['user'])) return false; // default dokuwiki does not put username here, let DW handle it 10974b4d4a4SAndreas Gohr $session->setUser($userdata, false); // does a login without resetting the time 11074b4d4a4SAndreas Gohr return true; 11174b4d4a4SAndreas Gohr } 11274b4d4a4SAndreas Gohr 11374b4d4a4SAndreas Gohr /** 1146d9a8a49SAndreas Gohr * Login based on user cookie and a previously saved access token 11574b4d4a4SAndreas Gohr * 1166d9a8a49SAndreas Gohr * @return bool true if successful, false if not applies 117c82ad624SAndreas Gohr * @throws \OAuth\Common\Exception\Exception 11874b4d4a4SAndreas Gohr */ 11974b4d4a4SAndreas Gohr protected function loginByCookie() 12074b4d4a4SAndreas Gohr { 12174b4d4a4SAndreas Gohr $session = Session::getInstance(); 12274b4d4a4SAndreas Gohr $cookie = $session->getCookie(); 12374b4d4a4SAndreas Gohr if (!$cookie) return false; 12474b4d4a4SAndreas Gohr 12574b4d4a4SAndreas Gohr $service = $this->loadService($cookie['servicename']); 12628002081SAndreas Gohr $service->initOAuthService($cookie['storageId']); 127c82ad624SAndreas Gohr 128c82ad624SAndreas Gohr // ensure that we have a current access token 1299cbef4d7SAndreas Gohr $service->refreshOutdatedToken(); 13074b4d4a4SAndreas Gohr 1316d9a8a49SAndreas Gohr // this should use a previously saved token 1326d9a8a49SAndreas Gohr $userdata = $service->getUser(); 1336d9a8a49SAndreas Gohr 1346d9a8a49SAndreas Gohr // processing 1356d9a8a49SAndreas Gohr $userdata = $this->validateUserData($userdata, $cookie['servicename']); 1366d9a8a49SAndreas Gohr $userdata = $this->processUserData($userdata, $cookie['servicename']); 1376d9a8a49SAndreas Gohr 13874b4d4a4SAndreas Gohr $session->setUser($userdata); // log in 13974b4d4a4SAndreas Gohr return true; 14074b4d4a4SAndreas Gohr } 14174b4d4a4SAndreas Gohr 142a1fa007aSNaoto Kobayashi /** 143a1fa007aSNaoto Kobayashi * Callback service's logout 144a1fa007aSNaoto Kobayashi * 145a1fa007aSNaoto Kobayashi * @return void 146a1fa007aSNaoto Kobayashi */ 147a1fa007aSNaoto Kobayashi public function logout() 148a1fa007aSNaoto Kobayashi { 149a1fa007aSNaoto Kobayashi $session = Session::getInstance(); 150a1fa007aSNaoto Kobayashi $cookie = $session->getCookie(); 151a1fa007aSNaoto Kobayashi if (!$cookie) return; 152a1fa007aSNaoto Kobayashi try { 153a1fa007aSNaoto Kobayashi $service = $this->loadService($cookie['servicename']); 154a1fa007aSNaoto Kobayashi $service->initOAuthService($cookie['storageId']); 155a1fa007aSNaoto Kobayashi $service->logout(); 156a1fa007aSNaoto Kobayashi } catch (\OAuth\Common\Exception\Exception $e) { 157a1fa007aSNaoto Kobayashi return; 158a1fa007aSNaoto Kobayashi } 159a1fa007aSNaoto Kobayashi } 160a1fa007aSNaoto Kobayashi 1616d9a8a49SAndreas Gohr // endregion 1626d9a8a49SAndreas Gohr 16374b4d4a4SAndreas Gohr /** 16428002081SAndreas Gohr * The ID we store authentication data as 16528002081SAndreas Gohr * 16628002081SAndreas Gohr * @param string $mail 16728002081SAndreas Gohr * @return string 16828002081SAndreas Gohr */ 16928002081SAndreas Gohr protected function getStorageId($mail) 17028002081SAndreas Gohr { 17128002081SAndreas Gohr return md5($mail); 17228002081SAndreas Gohr } 17328002081SAndreas Gohr 17428002081SAndreas Gohr /** 17574b4d4a4SAndreas Gohr * Clean and validate the user data provided from the service 17674b4d4a4SAndreas Gohr * 17774b4d4a4SAndreas Gohr * @param array $userdata 17874b4d4a4SAndreas Gohr * @param string $servicename 17974b4d4a4SAndreas Gohr * @return array 18074b4d4a4SAndreas Gohr * @throws Exception 18174b4d4a4SAndreas Gohr */ 18274b4d4a4SAndreas Gohr protected function validateUserData($userdata, $servicename) 18374b4d4a4SAndreas Gohr { 18474b4d4a4SAndreas Gohr /** @var \auth_plugin_oauth */ 18574b4d4a4SAndreas Gohr global $auth; 18674b4d4a4SAndreas Gohr 18774b4d4a4SAndreas Gohr // mail is required 18874b4d4a4SAndreas Gohr if (empty($userdata['mail'])) { 189d1826331SAndreas Gohr throw new Exception('noEmail', [$servicename]); 19074b4d4a4SAndreas Gohr } 19174b4d4a4SAndreas Gohr 192e261c7e8SAndreas Gohr $userdata['mail'] = strtolower($userdata['mail']); 193e261c7e8SAndreas Gohr 19474b4d4a4SAndreas Gohr // mail needs to be allowed 19574b4d4a4SAndreas Gohr /** @var \helper_plugin_oauth $hlp */ 19674b4d4a4SAndreas Gohr $hlp = plugin_load('helper', 'oauth'); 19739730c7eSAnna Dabrowska 19839730c7eSAnna Dabrowska if (!$hlp->checkMail($userdata['mail'])) { 199d1826331SAndreas Gohr throw new Exception('rejectedEMail', [join(', ', $hlp->getValidDomains())]); 20039730c7eSAnna Dabrowska } 20174b4d4a4SAndreas Gohr 20274b4d4a4SAndreas Gohr // make username from mail if empty 2031a5ede3eSAndreas Gohr if (!isset($userdata['user'])) $userdata['user'] = ''; 20474b4d4a4SAndreas Gohr $userdata['user'] = $auth->cleanUser((string)$userdata['user']); 2051a5ede3eSAndreas Gohr if ($userdata['user'] === '') { 20674b4d4a4SAndreas Gohr list($userdata['user']) = explode('@', $userdata['mail']); 20774b4d4a4SAndreas Gohr } 20874b4d4a4SAndreas Gohr 20974b4d4a4SAndreas Gohr // make full name from username if empty 21074b4d4a4SAndreas Gohr if (empty($userdata['name'])) { 21174b4d4a4SAndreas Gohr $userdata['name'] = $userdata['user']; 21274b4d4a4SAndreas Gohr } 21374b4d4a4SAndreas Gohr 2141a5ede3eSAndreas Gohr // make sure groups are array and valid 2151a5ede3eSAndreas Gohr if (!isset($userdata['grps'])) $userdata['grps'] = []; 2161a5ede3eSAndreas Gohr $userdata['grps'] = array_map([$auth, 'cleanGroup'], (array)$userdata['grps']); 2171a5ede3eSAndreas Gohr 21874b4d4a4SAndreas Gohr return $userdata; 21974b4d4a4SAndreas Gohr } 22074b4d4a4SAndreas Gohr 22174b4d4a4SAndreas Gohr /** 22274b4d4a4SAndreas Gohr * Process the userdata, update the user info array and create the user if necessary 22374b4d4a4SAndreas Gohr * 22474b4d4a4SAndreas Gohr * Uses the global $auth object for user management 22574b4d4a4SAndreas Gohr * 22674b4d4a4SAndreas Gohr * @param array $userdata User info received from authentication 22774b4d4a4SAndreas Gohr * @param string $servicename Auth service 22874b4d4a4SAndreas Gohr * @return array the modified user info 22974b4d4a4SAndreas Gohr * @throws Exception 23074b4d4a4SAndreas Gohr */ 23174b4d4a4SAndreas Gohr protected function processUserData($userdata, $servicename) 23274b4d4a4SAndreas Gohr { 233e170f465SAndreas Gohr /** @var \auth_plugin_oauth $auth */ 23474b4d4a4SAndreas Gohr global $auth; 23574b4d4a4SAndreas Gohr 23674b4d4a4SAndreas Gohr // see if the user is known already 23774b4d4a4SAndreas Gohr $localUser = $auth->getUserByEmail($userdata['mail']); 23874b4d4a4SAndreas Gohr if ($localUser) { 23974b4d4a4SAndreas Gohr $localUserInfo = $auth->getUserData($localUser); 240d209a58cSAndreas Gohr $localUserInfo['user'] = $localUser; 241d209a58cSAndreas Gohr if(isset($localUserInfo['pass'])) unset($localUserInfo['pass']); 242d209a58cSAndreas Gohr 24374b4d4a4SAndreas Gohr // check if the user allowed access via this service 24474b4d4a4SAndreas Gohr if (!in_array($auth->cleanGroup($servicename), $localUserInfo['grps'])) { 245d1826331SAndreas Gohr throw new Exception('authnotenabled', [$servicename]); 24674b4d4a4SAndreas Gohr } 247f81e58d4SAndreas Gohr 248f81e58d4SAndreas Gohr $helper = plugin_load('helper', 'oauth'); 249f81e58d4SAndreas Gohr 25074b4d4a4SAndreas Gohr $userdata['user'] = $localUser; 25174b4d4a4SAndreas Gohr $userdata['name'] = $localUserInfo['name']; 252f81e58d4SAndreas Gohr $userdata['grps'] = $this->mergeGroups( 253f81e58d4SAndreas Gohr $localUserInfo['grps'], 254f81e58d4SAndreas Gohr $userdata['grps'] ?? [], 255f81e58d4SAndreas Gohr array_keys($helper->listServices(false)), 256f81e58d4SAndreas Gohr $auth->getConf('overwrite-groups') 257f81e58d4SAndreas Gohr ); 2581e4efa57SAnna Dabrowska 259d209a58cSAndreas Gohr // update user if changed 260*bfebac18SAndreas Gohr sort($localUserInfo['grps']); 261*bfebac18SAndreas Gohr sort($userdata['grps']); 262*bfebac18SAndreas Gohr if ($localUserInfo != $userdata && !isset($localUserInfo['protected'])) { 2631e4efa57SAnna Dabrowska $auth->modifyUser($localUser, $userdata); 264d209a58cSAndreas Gohr } 26574b4d4a4SAndreas Gohr } elseif (actionOK('register') || $auth->getConf('register-on-auth')) { 266e170f465SAndreas Gohr if (!$auth->registerOAuthUser($userdata, $servicename)) { 267d1826331SAndreas Gohr throw new Exception('generic create error'); 26874b4d4a4SAndreas Gohr } 26974b4d4a4SAndreas Gohr } else { 270d1826331SAndreas Gohr throw new Exception('addUser not possible'); 27174b4d4a4SAndreas Gohr } 27274b4d4a4SAndreas Gohr 27374b4d4a4SAndreas Gohr return $userdata; 27474b4d4a4SAndreas Gohr } 27574b4d4a4SAndreas Gohr 27674b4d4a4SAndreas Gohr /** 277ad56356cSAnna Dabrowska * Merges local and provider user groups. Keeps internal 278ad56356cSAnna Dabrowska * Dokuwiki groups unless configured to overwrite all ('overwrite-groups' setting) 279ad56356cSAnna Dabrowska * 280f81e58d4SAndreas Gohr * @param string[] $localGroups Local user groups 281f81e58d4SAndreas Gohr * @param string[] $providerGroups Groups fetched from the provider 282f81e58d4SAndreas Gohr * @param string[] $servicenames Service names that should be kept if set 283ad56356cSAnna Dabrowska * @param bool $overwrite Config setting to overwrite local DokuWiki groups 284ad56356cSAnna Dabrowska * 285ad56356cSAnna Dabrowska * @return array 286ad56356cSAnna Dabrowska */ 287f81e58d4SAndreas Gohr protected function mergeGroups($localGroups, $providerGroups, $servicenames, $overwrite) 288ad56356cSAnna Dabrowska { 289ad56356cSAnna Dabrowska global $conf; 290ad56356cSAnna Dabrowska 291f81e58d4SAndreas Gohr // overwrite-groups set in config - remove all local groups except services and default 292f81e58d4SAndreas Gohr if ($overwrite) { 293f81e58d4SAndreas Gohr $localGroups = array_intersect($localGroups, array_merge($servicenames, [$conf['defaultgroup']])); 294f81e58d4SAndreas Gohr } 295f81e58d4SAndreas Gohr 296f81e58d4SAndreas Gohr return array_unique(array_merge($localGroups, $providerGroups)); 297ad56356cSAnna Dabrowska } 298ad56356cSAnna Dabrowska 299ad56356cSAnna Dabrowska /** 30074b4d4a4SAndreas Gohr * Instantiates a Service by name 30174b4d4a4SAndreas Gohr * 30274b4d4a4SAndreas Gohr * @param string $servicename 30304a78b87SAndreas Gohr * @return Adapter 30474b4d4a4SAndreas Gohr * @throws Exception 30574b4d4a4SAndreas Gohr */ 30674b4d4a4SAndreas Gohr protected function loadService($servicename) 30774b4d4a4SAndreas Gohr { 30874b4d4a4SAndreas Gohr /** @var \helper_plugin_oauth $hlp */ 30974b4d4a4SAndreas Gohr $hlp = plugin_load('helper', 'oauth'); 31074b4d4a4SAndreas Gohr $srv = $hlp->loadService($servicename); 31174b4d4a4SAndreas Gohr 31274b4d4a4SAndreas Gohr if ($srv === null) throw new Exception("No such service $servicename"); 31374b4d4a4SAndreas Gohr return $srv; 31474b4d4a4SAndreas Gohr } 31574b4d4a4SAndreas Gohr 31674b4d4a4SAndreas Gohr} 317