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