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