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