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