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