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