xref: /plugin/oauth/auth.php (revision 6f3a59e8e507433aa78067cb8d1475ba77fa05f7)
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