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