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