1<?php
2
3namespace dokuwiki\plugin\oauth;
4
5/**
6 * Implements the flow control for oAuth
7 */
8class OAuthManager
9{
10    // region main flow
11
12    /**
13     * Explicitly starts the oauth flow by redirecting to IDP
14     *
15     * @throws \OAuth\Common\Exception\Exception
16     */
17    public function startFlow($servicename)
18    {
19        global $ID;
20
21        $session = Session::getInstance();
22        $session->setLoginData($servicename, $ID);
23        $service = $this->loadService($servicename);
24        $service->initOAuthService();
25        $service->login(); // redirects
26    }
27
28    /**
29     * Continues the flow from various states
30     *
31     * @return bool true if the login has been handled
32     * @throws Exception
33     * @throws \OAuth\Common\Exception\Exception
34     */
35    public function continueFlow()
36    {
37        return $this->loginByService() or
38            $this->loginBySession() or
39            $this->loginByCookie();
40    }
41
42    /**
43     * Second step in a explicit login, validates the oauth code
44     *
45     * @return bool true if successful, false if not applies
46     * @throws \OAuth\Common\Exception\Exception
47     */
48    protected function loginByService()
49    {
50        global $INPUT;
51
52        if (!$INPUT->get->has('code') && !$INPUT->get->has('oauth_token')) {
53            return false;
54        }
55
56        $session = Session::getInstance();
57
58        // init service from session
59        $logindata = $session->getLoginData();
60        if (!$logindata) return false;
61        $service = $this->loadService($logindata['servicename']);
62        $service->initOAuthService();
63        $session->clearLoginData();
64
65        // oAuth login
66        if (!$service->checkToken()) throw new \OAuth\Common\Exception\Exception("Invalid Token - Login failed");
67        $userdata = $service->getUser();
68
69        // processing
70        $userdata = $this->validateUserData($userdata, $logindata['servicename']);
71        $userdata = $this->processUserData($userdata, $logindata['servicename']);
72
73        // store data
74        $storageId = $this->getStorageId($userdata['mail']);
75        $service->upgradeStorage($storageId);
76
77        // login
78        $session->setUser($userdata); // log in
79        $session->setCookie($logindata['servicename'], $storageId); // set cookie
80
81        // redirect to the appropriate ID
82        if (!empty($logindata['id'])) {
83            send_redirect(wl($logindata['id'], [], true, '&'));
84        }
85        return true;
86    }
87
88    /**
89     * Login based on user's current session data
90     *
91     * This will also log in plainauth users
92     *
93     * @return bool true if successful, false if not applies
94     * @throws Exception
95     */
96    protected function loginBySession()
97    {
98        $session = Session::getInstance();
99        if (!$session->isValid()) {
100            $session->clear();
101            return false;
102        }
103
104        $userdata = $session->getUser();
105        if (!$userdata) return false;
106        if (!isset($userdata['user'])) return false; // default dokuwiki does not put username here, let DW handle it
107        $session->setUser($userdata, false); // does a login without resetting the time
108        return true;
109    }
110
111    /**
112     * Login based on user cookie and a previously saved access token
113     *
114     * @return bool true if successful, false if not applies
115     * @throws \OAuth\Common\Exception\Exception
116     */
117    protected function loginByCookie()
118    {
119        $session = Session::getInstance();
120        $cookie = $session->getCookie();
121        if (!$cookie) return false;
122
123        $service = $this->loadService($cookie['servicename']);
124        $service->initOAuthService($cookie['storageId']);
125
126        // ensure that we have a current access token
127        $service->refreshOutdatedToken();
128
129        // this should use a previously saved token
130        $userdata = $service->getUser();
131
132        // processing
133        $userdata = $this->validateUserData($userdata, $cookie['servicename']);
134        $userdata = $this->processUserData($userdata, $cookie['servicename']);
135
136        $session->setUser($userdata); // log in
137        return true;
138    }
139
140    /**
141     * Callback service's logout
142     *
143     * @return void
144     */
145    public function logout()
146    {
147        $session = Session::getInstance();
148        $cookie = $session->getCookie();
149        if (!$cookie) return;
150        try {
151            $service = $this->loadService($cookie['servicename']);
152            $service->initOAuthService($cookie['storageId']);
153            $service->logout();
154        } catch (\OAuth\Common\Exception\Exception $e) {
155            return;
156        }
157    }
158
159    // endregion
160
161    /**
162     * The ID we store authentication data as
163     *
164     * @param string $mail
165     * @return string
166     */
167    protected function getStorageId($mail)
168    {
169        return md5($mail);
170    }
171
172    /**
173     * Clean and validate the user data provided from the service
174     *
175     * @param array $userdata
176     * @param string $servicename
177     * @return array
178     * @throws Exception
179     */
180    protected function validateUserData($userdata, $servicename)
181    {
182        /** @var \auth_plugin_oauth */
183        global $auth;
184
185        // mail is required
186        if (empty($userdata['mail'])) {
187            throw new Exception('noEmail', [$servicename]);
188        }
189
190        $userdata['mail'] = strtolower($userdata['mail']);
191
192        // mail needs to be allowed
193        /** @var \helper_plugin_oauth $hlp */
194        $hlp = plugin_load('helper', 'oauth');
195
196        if (!$hlp->checkMail($userdata['mail'])) {
197            throw new Exception('rejectedEMail', [join(', ', $hlp->getValidDomains())]);
198        }
199
200        // make username from mail if empty
201        if (!isset($userdata['user'])) $userdata['user'] = '';
202        $userdata['user'] = $auth->cleanUser((string)$userdata['user']);
203        if ($userdata['user'] === '') {
204            list($userdata['user']) = explode('@', $userdata['mail']);
205        }
206
207        // make full name from username if empty
208        if (empty($userdata['name'])) {
209            $userdata['name'] = $userdata['user'];
210        }
211
212        // make sure groups are array and valid
213        if (!isset($userdata['grps'])) $userdata['grps'] = [];
214        $userdata['grps'] = array_map([$auth, 'cleanGroup'], (array)$userdata['grps']);
215
216        return $userdata;
217    }
218
219    /**
220     * Process the userdata, update the user info array and create the user if necessary
221     *
222     * Uses the global $auth object for user management
223     *
224     * @param array $userdata User info received from authentication
225     * @param string $servicename Auth service
226     * @return array the modified user info
227     * @throws Exception
228     */
229    protected function processUserData($userdata, $servicename)
230    {
231        /** @var \auth_plugin_oauth $auth */
232        global $auth;
233
234        // see if the user is known already
235        $localUser = $auth->getUserByEmail($userdata['mail']);
236        if ($localUser) {
237            $localUserInfo = $auth->getUserData($localUser);
238            // check if the user allowed access via this service
239            if (!in_array($auth->cleanGroup($servicename), $localUserInfo['grps'])) {
240                throw new Exception('authnotenabled', [$servicename]);
241            }
242            $userdata['user'] = $localUser;
243            $userdata['name'] = $localUserInfo['name'];
244            $userdata['grps'] = array_merge((array)$userdata['grps'], $localUserInfo['grps']);
245        } elseif (actionOK('register') || $auth->getConf('register-on-auth')) {
246            if (!$auth->registerOAuthUser($userdata, $servicename)) {
247                throw new Exception('generic create error');
248            }
249        } else {
250            throw new Exception('addUser not possible');
251        }
252
253        return $userdata;
254    }
255
256    /**
257     * Instantiates a Service by name
258     *
259     * @param string $servicename
260     * @return Adapter
261     * @throws Exception
262     */
263    protected function loadService($servicename)
264    {
265        /** @var \helper_plugin_oauth $hlp */
266        $hlp = plugin_load('helper', 'oauth');
267        $srv = $hlp->loadService($servicename);
268
269        if ($srv === null) throw new Exception("No such service $servicename");
270        return $srv;
271    }
272
273}
274