1<?php 2 3use dokuwiki\plugin\oauth\SessionManager; 4 5/** 6 * DokuWiki Plugin oauth (Auth Component) 7 * 8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 9 * @author Andreas Gohr <andi@splitbrain.org> 10 */ 11class auth_plugin_oauth extends auth_plugin_authplain 12{ 13 14 /** @inheritDoc */ 15 public function __construct() 16 { 17 parent::__construct(); 18 19 $this->cando['external'] = true; 20 } 21 22 /** @inheritDoc */ 23 public function trustExternal($user, $pass, $sticky = false) 24 { 25 global $INPUT; 26 27 // handle redirects from farmer to animal wiki instances 28 if ($INPUT->has('state') && plugin_load('helper', 'farmer', false, true)) { 29 $this->handleState($INPUT->str('state')); 30 } 31 32 // first check in auth setup: is auth data present and still valid? 33 if ($this->sessionLogin()) return true; 34 35 // if we have a service in session, either we're in oauth login or a previous login needs to be revalidated 36 $servicename = SessionManager::getServiceName(); 37 if ($servicename) { 38 return $this->serviceLogin($servicename, 39 $sticky, 40 SessionManager::getPid(), 41 SessionManager::getParams(), 42 SessionManager::hasState() 43 ); 44 } 45 46 // otherwise try cookie 47 $this->cookieLogin(); 48 49 // do the "normal" plain auth login via form 50 return auth_login($user, $pass, $sticky); 51 } 52 53 /** 54 * Enhance function to check against duplicate emails 55 * 56 * @param string $user 57 * @param string $pwd 58 * @param string $name 59 * @param string $mail 60 * @param null $grps 61 * @return bool|null|string 62 */ 63 public function createUser($user, $pwd, $name, $mail, $grps = null) 64 { 65 if ($this->getUserByEmail($mail)) { 66 msg($this->getLang('emailduplicate'), -1); 67 return false; 68 } 69 70 return parent::createUser($user, $pwd, $name, $mail, $grps); 71 } 72 73 /** 74 * Enhance function to check against duplicate emails 75 * 76 * @param string $user 77 * @param array $changes 78 * @return bool 79 */ 80 public function modifyUser($user, $changes) 81 { 82 global $conf; 83 84 if (isset($changes['mail'])) { 85 $found = $this->getUserByEmail($changes['mail']); 86 if ($found && $found != $user) { 87 msg($this->getLang('emailduplicate'), -1); 88 return false; 89 } 90 } 91 92 $ok = parent::modifyUser($user, $changes); 93 94 // refresh session cache 95 touch($conf['cachedir'] . '/sessionpurge'); 96 97 return $ok; 98 } 99 100 /** 101 * Unset additional stuff in session on logout 102 */ 103 public function logOff() 104 { 105 parent::logOff(); 106 107 $this->cleanLogout(); 108 } 109 110 /** 111 * check if auth data is present in session and is still considered valid 112 * 113 * @return bool 114 */ 115 protected function sessionLogin() 116 { 117 global $USERINFO; 118 $session = $_SESSION[DOKU_COOKIE]['auth']; 119 // FIXME session can be null at this point (e.g. coming from sprintdoc svg.php) 120 // FIXME and so the subsequent check for non-GET non-doku.php requests is not performed 121 if (isset($session['oauth']) && $this->isSessionValid($session)) { 122 $_SERVER['REMOTE_USER'] = $session['user']; 123 $USERINFO = $session['info']; 124 return true; 125 } 126 return false; 127 } 128 129 /** 130 * Use cookie data to log in 131 */ 132 protected function cookieLogin() 133 { 134 if (isset($_COOKIE[DOKU_COOKIE])) { 135 list($cookieuser, $cookiesticky, $auth, $servicename) = explode('|', $_COOKIE[DOKU_COOKIE]); 136 $auth = base64_decode($auth, true); 137 $servicename = base64_decode($servicename, true); 138 if ($auth === 'oauth') { 139 $this->relogin($servicename); 140 } 141 } 142 } 143 144 /** 145 * Use the OAuth service 146 * 147 * @param $servicename 148 * @param $sticky 149 * @param $page 150 * @param $params 151 * @param $existingLoginProcess 152 * @return bool 153 * @throws \OAuth\Common\Exception\Exception 154 * @throws \OAuth\Common\Http\Exception\TokenResponseException 155 * @throws \OAuth\Common\Storage\Exception\TokenNotFoundException 156 */ 157 protected function serviceLogin($servicename, $sticky, $page, $params, $existingLoginProcess) 158 { 159 $service = $this->getService($servicename); 160 if (is_null($service)) { 161 $this->cleanLogout(); 162 return false; 163 } 164 165 if ($service->checkToken()) { 166 if (!$this->processLogin($sticky, $service, $servicename, $page, $params)) { 167 $this->cleanLogout(); 168 return false; 169 } 170 return true; 171 } else { 172 if ($existingLoginProcess) { 173 msg($this->getLang('oauth login failed'), 0); 174 $this->cleanLogout(); 175 return false; 176 } else { 177 // first time here 178 $this->relogin($servicename); 179 } 180 } 181 182 $this->cleanLogout(); 183 return false; // something went wrong during oAuth login 184 } 185 186 /** 187 * Relogin using auth info read from session / cookie 188 * 189 * @param string $servicename 190 * @return void|false 191 * @throws \OAuth\Common\Http\Exception\TokenResponseException 192 */ 193 protected function relogin($servicename) 194 { 195 $service = $this->getService($servicename); 196 if (is_null($service)) return false; 197 198 $this->writeSession($servicename); 199 $service->login(); 200 } 201 202 203 /** 204 * @param bool $sticky 205 * @param \dokuwiki\plugin\oauth\Service $service 206 * @param string $servicename 207 * @param string $page 208 * @param array $params 209 * 210 * @return bool 211 * @throws \OAuth\Common\Exception\Exception 212 */ 213 protected function processLogin($sticky, $service, $servicename, $page, $params = []) 214 { 215 $userinfo = $service->getUser(); 216 $ok = $this->processUserinfo($userinfo, $servicename); 217 if (!$ok) { 218 return false; 219 } 220 $this->setUserSession($userinfo, $servicename); 221 $this->setUserCookie($userinfo['user'], $sticky, $servicename); 222 if (isset($page)) { 223 if (!empty($params['id'])) unset($params['id']); 224 send_redirect(wl($page, $params, false, '&')); 225 } 226 return true; 227 } 228 229 /** 230 * process the user and update the user info array 231 * 232 * @param array $userinfo User info received from authentication 233 * @param string $servicename Auth service 234 * 235 * @return bool 236 */ 237 protected function processUserinfo(&$userinfo, $servicename) 238 { 239 $userinfo['user'] = $this->cleanUser((string)$userinfo['user']); 240 if (!$userinfo['name']) $userinfo['name'] = $userinfo['user']; 241 242 if (!$userinfo['user'] || !$userinfo['mail']) { 243 msg("$servicename did not provide the needed user info. Can't log you in", -1); 244 return false; 245 } 246 247 // see if the user is known already 248 $localUser = $this->getUserByEmail($userinfo['mail']); 249 if ($localUser) { 250 $localUserInfo = $this->getUserData($localUser); 251 // check if the user allowed access via this service 252 if (!in_array($this->cleanGroup($servicename), $localUserInfo['grps'])) { 253 msg(sprintf($this->getLang('authnotenabled'), $servicename), -1); 254 return false; 255 } 256 $userinfo['user'] = $localUser; 257 $userinfo['name'] = $localUserInfo['name']; 258 $userinfo['grps'] = array_merge((array)$userinfo['grps'], $localUserInfo['grps']); 259 } elseif (actionOK('register') || $this->getConf('register-on-auth')) { 260 $ok = $this->addUser($userinfo, $servicename); 261 if (!$ok) { 262 msg('something went wrong creating your user account. please try again later.', -1); 263 return false; 264 } 265 } else { 266 msg($this->getLang('addUser not possible'), -1); 267 return false; 268 } 269 return true; 270 } 271 272 /** 273 * new user, create him - making sure the login is unique by adding a number if needed 274 * 275 * @param array $userinfo user info received from the oAuth service 276 * @param string $servicename 277 * 278 * @return bool 279 */ 280 protected function addUser(&$userinfo, $servicename) 281 { 282 global $conf; 283 $user = $userinfo['user']; 284 $count = ''; 285 while ($this->getUserData($user . $count)) { 286 if ($count) { 287 $count++; 288 } else { 289 $count = 1; 290 } 291 } 292 $user = $user . $count; 293 $userinfo['user'] = $user; 294 $groups_on_creation = array(); 295 $groups_on_creation[] = $conf['defaultgroup']; 296 $groups_on_creation[] = $this->cleanGroup($servicename); // add service as group 297 $userinfo['grps'] = array_merge((array)$userinfo['grps'], $groups_on_creation); 298 299 $ok = $this->triggerUserMod( 300 'create', 301 array($user, auth_pwgen($user), $userinfo['name'], $userinfo['mail'], $groups_on_creation,) 302 ); 303 if (!$ok) { 304 return false; 305 } 306 307 // send notification about the new user 308 $subscription = new Subscription(); 309 $subscription->send_register($user, $userinfo['name'], $userinfo['mail']); 310 return true; 311 } 312 313 /** 314 * Find a user by email address 315 * 316 * @param $mail 317 * @return bool|string 318 */ 319 protected function getUserByEmail($mail) 320 { 321 if ($this->users === null) { 322 if (is_callable([$this, '_loadUserData'])) { 323 $this->_loadUserData(); 324 } else { 325 $this->loadUserData(); 326 } 327 } 328 $mail = strtolower($mail); 329 330 foreach ($this->users as $user => $userinfo) { 331 if (strtolower($userinfo['mail']) == $mail) return $user; 332 } 333 334 return false; 335 } 336 337 /** 338 * unset auth cookies and session information 339 */ 340 private function cleanLogout() 341 { 342 if (isset($_SESSION[DOKU_COOKIE]['oauth-done'])) { 343 unset($_SESSION[DOKU_COOKIE]['oauth-done']); 344 } 345 if (isset($_SESSION[DOKU_COOKIE]['auth'])) { 346 unset($_SESSION[DOKU_COOKIE]['auth']); 347 } 348 $this->setUserCookie('', true, '', -60); 349 } 350 351 /** 352 * @param string $servicename 353 * @return \dokuwiki\plugin\oauth\Service 354 */ 355 protected function getService($servicename) 356 { 357 /** @var helper_plugin_oauth $hlp */ 358 $hlp = plugin_load('helper', 'oauth'); 359 360 return $hlp->loadService($servicename); 361 } 362 363 364 /** 365 * Save user and auth data 366 * 367 * @param array $data 368 * @param string $service 369 */ 370 protected function setUserSession($data, $service) 371 { 372 global $USERINFO; 373 374 // set up groups 375 if (!is_array($data['grps'])) { 376 $data['grps'] = array(); 377 } 378 $data['grps'][] = $this->cleanGroup($service); 379 $data['grps'] = array_unique($data['grps']); 380 381 $USERINFO = $data; 382 $_SERVER['REMOTE_USER'] = $data['user']; 383 $_SESSION[DOKU_COOKIE]['auth']['user'] = $data['user']; 384 $_SESSION[DOKU_COOKIE]['auth']['pass'] = $data['pass']; 385 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 386 $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid(); 387 $_SESSION[DOKU_COOKIE]['auth']['time'] = time(); 388 $_SESSION[DOKU_COOKIE]['auth']['oauth'] = $service; 389 } 390 391 /** 392 * @param string $user 393 * @param bool $sticky 394 * @param string $servicename 395 * @param int $validityPeriodInSeconds optional, per default 1 Year 396 */ 397 private function setUserCookie($user, $sticky, $servicename, $validityPeriodInSeconds = 31536000) 398 { 399 $cookie = base64_encode($user) . '|' . ((int)$sticky) . '|' . base64_encode('oauth') . '|' . base64_encode($servicename); 400 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 401 $time = $sticky ? (time() + $validityPeriodInSeconds) : 0; 402 setcookie(DOKU_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 403 } 404 405 /** 406 * @param array $session cookie auth session 407 * 408 * @return bool 409 */ 410 protected function isSessionValid($session) 411 { 412 /** @var helper_plugin_oauth $hlp */ 413 $hlp = plugin_load('helper', 'oauth'); 414 if ($hlp->validBrowserID($session)) { 415 if (!$hlp->isSessionTimedOut($session)) { 416 return true; 417 } elseif (!($hlp->isGETRequest() && $hlp->isDokuPHP())) { 418 // only force a recheck on a timed-out session during a GET request on the main script doku.php 419 return true; 420 } 421 } 422 return false; 423 } 424 425 /** 426 * Save login info in session 427 * 428 * @param string $servicename 429 */ 430 protected function writeSession($servicename) 431 { 432 global $INPUT; 433 434 // FIXME delegate to SessionManager? in action/login.php as well? 435 session_start(); 436 $_SESSION[DOKU_COOKIE]['oauth-inprogress']['service'] = $servicename; 437 $_SESSION[DOKU_COOKIE]['oauth-inprogress']['id'] = $INPUT->str('id'); 438 439 $_SESSION[DOKU_COOKIE]['oauth-inprogress']['params'] = $_GET; 440 441 $_SESSION[DOKU_COOKIE]['oauth-done']['$_REQUEST'] = $_REQUEST; 442 443 if (is_array($INPUT->post->param('do'))) { 444 $doPost = key($INPUT->post->arr('do')); 445 } else { 446 $doPost = $INPUT->post->str('do'); 447 } 448 $doGet = $INPUT->get->str('do'); 449 if (!empty($doPost)) { 450 $_SESSION[DOKU_COOKIE]['oauth-done']['do'] = $doPost; 451 } elseif (!empty($doGet)) { 452 $_SESSION[DOKU_COOKIE]['oauth-done']['do'] = $doGet; 453 } 454 455 session_write_close(); 456 } 457 458 /** 459 * Farmer plugin support 460 * 461 * When coming back to farmer instance via OAUTH redirectURI, we need to redirect again 462 * to a proper animal instance detected from $state 463 * 464 * @param $state 465 */ 466 private function handleState($state) 467 { 468 /** @var \helper_plugin_farmer $farmer */ 469 $farmer = plugin_load('helper', 'farmer', false, true); 470 $data = json_decode(base64_decode(urldecode($state))); 471 if (empty($data->animal) || $farmer->getAnimal() == $data->animal) { 472 return; 473 } 474 $animal = $data->animal; 475 $allAnimals = $farmer->getAllAnimals(); 476 if (!in_array($animal, $allAnimals)) { 477 msg('Animal ' . $animal . ' does not exist!'); 478 return; 479 } 480 global $INPUT; 481 $url = $farmer->getAnimalURL($animal) . '/doku.php?' . $INPUT->server->str('QUERY_STRING'); 482 send_redirect($url); 483 } 484} 485 486// vim:ts=4:sw=4:et: 487