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