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