xref: /plugin/oauth/OAuthManager.php (revision d209a58c7b604858790e500a284a0d95effc1498)
174b4d4a4SAndreas Gohr<?php
274b4d4a4SAndreas Gohr
374b4d4a4SAndreas Gohrnamespace dokuwiki\plugin\oauth;
474b4d4a4SAndreas Gohr
56d9a8a49SAndreas Gohr/**
66d9a8a49SAndreas Gohr * Implements the flow control for oAuth
76d9a8a49SAndreas Gohr */
874b4d4a4SAndreas Gohrclass OAuthManager
974b4d4a4SAndreas Gohr{
106d9a8a49SAndreas Gohr    // region main flow
1174b4d4a4SAndreas Gohr
1274b4d4a4SAndreas Gohr    /**
1331039e80SAndreas Gohr     * Explicitly starts the oauth flow by redirecting to IDP
1431039e80SAndreas Gohr     *
1504a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
1674b4d4a4SAndreas Gohr     */
1774b4d4a4SAndreas Gohr    public function startFlow($servicename)
1874b4d4a4SAndreas Gohr    {
1931039e80SAndreas Gohr        global $ID;
2031039e80SAndreas Gohr
2174b4d4a4SAndreas Gohr        $session = Session::getInstance();
2228002081SAndreas Gohr        $session->setLoginData($servicename, $ID);
2374b4d4a4SAndreas Gohr        $service = $this->loadService($servicename);
2428002081SAndreas Gohr        $service->initOAuthService();
2574b4d4a4SAndreas Gohr        $service->login(); // redirects
2674b4d4a4SAndreas Gohr    }
2774b4d4a4SAndreas Gohr
2874b4d4a4SAndreas Gohr    /**
2931039e80SAndreas Gohr     * Continues the flow from various states
3031039e80SAndreas Gohr     *
3174b4d4a4SAndreas Gohr     * @return bool true if the login has been handled
3274b4d4a4SAndreas Gohr     * @throws Exception
3374b4d4a4SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
3474b4d4a4SAndreas Gohr     */
3574b4d4a4SAndreas Gohr    public function continueFlow()
3674b4d4a4SAndreas Gohr    {
3774b4d4a4SAndreas Gohr        return $this->loginByService() or
3874b4d4a4SAndreas Gohr            $this->loginBySession() or
3974b4d4a4SAndreas Gohr            $this->loginByCookie();
4074b4d4a4SAndreas Gohr    }
4174b4d4a4SAndreas Gohr
4274b4d4a4SAndreas Gohr    /**
436d9a8a49SAndreas Gohr     * Second step in a explicit login, validates the oauth code
446d9a8a49SAndreas Gohr     *
456d9a8a49SAndreas Gohr     * @return bool true if successful, false if not applies
4674b4d4a4SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
4774b4d4a4SAndreas Gohr     */
4874b4d4a4SAndreas Gohr    protected function loginByService()
4974b4d4a4SAndreas Gohr    {
5074b4d4a4SAndreas Gohr        global $INPUT;
5174b4d4a4SAndreas Gohr
5274b4d4a4SAndreas Gohr        if (!$INPUT->get->has('code') && !$INPUT->get->has('oauth_token')) {
5374b4d4a4SAndreas Gohr            return false;
5474b4d4a4SAndreas Gohr        }
5574b4d4a4SAndreas Gohr
5674b4d4a4SAndreas Gohr        $session = Session::getInstance();
5774b4d4a4SAndreas Gohr
5874b4d4a4SAndreas Gohr        // init service from session
5974b4d4a4SAndreas Gohr        $logindata = $session->getLoginData();
6074b4d4a4SAndreas Gohr        if (!$logindata) return false;
6174b4d4a4SAndreas Gohr        $service = $this->loadService($logindata['servicename']);
6228002081SAndreas Gohr        $service->initOAuthService();
6374b4d4a4SAndreas Gohr        $session->clearLoginData();
6474b4d4a4SAndreas Gohr
6574b4d4a4SAndreas Gohr        // oAuth login
6604a78b87SAndreas Gohr        if (!$service->checkToken()) throw new \OAuth\Common\Exception\Exception("Invalid Token - Login failed");
6774b4d4a4SAndreas Gohr        $userdata = $service->getUser();
6874b4d4a4SAndreas Gohr
6974b4d4a4SAndreas Gohr        // processing
7074b4d4a4SAndreas Gohr        $userdata = $this->validateUserData($userdata, $logindata['servicename']);
7174b4d4a4SAndreas Gohr        $userdata = $this->processUserData($userdata, $logindata['servicename']);
7274b4d4a4SAndreas Gohr
7328002081SAndreas Gohr        // store data
7428002081SAndreas Gohr        $storageId = $this->getStorageId($userdata['mail']);
7528002081SAndreas Gohr        $service->upgradeStorage($storageId);
7628002081SAndreas Gohr
7774b4d4a4SAndreas Gohr        // login
7874b4d4a4SAndreas Gohr        $session->setUser($userdata); // log in
7928002081SAndreas Gohr        $session->setCookie($logindata['servicename'], $storageId); // set cookie
8074b4d4a4SAndreas Gohr
8131039e80SAndreas Gohr        // redirect to the appropriate ID
8231039e80SAndreas Gohr        if (!empty($logindata['id'])) {
8331039e80SAndreas Gohr            send_redirect(wl($logindata['id'], [], true, '&'));
8431039e80SAndreas Gohr        }
8574b4d4a4SAndreas Gohr        return true;
8674b4d4a4SAndreas Gohr    }
8774b4d4a4SAndreas Gohr
8874b4d4a4SAndreas Gohr    /**
896d9a8a49SAndreas Gohr     * Login based on user's current session data
9074b4d4a4SAndreas Gohr     *
9174b4d4a4SAndreas Gohr     * This will also log in plainauth users
9274b4d4a4SAndreas Gohr     *
936d9a8a49SAndreas Gohr     * @return bool true if successful, false if not applies
9474b4d4a4SAndreas Gohr     * @throws Exception
9574b4d4a4SAndreas Gohr     */
9674b4d4a4SAndreas Gohr    protected function loginBySession()
9774b4d4a4SAndreas Gohr    {
9874b4d4a4SAndreas Gohr        $session = Session::getInstance();
9974b4d4a4SAndreas Gohr        if (!$session->isValid()) {
10074b4d4a4SAndreas Gohr            $session->clear();
10174b4d4a4SAndreas Gohr            return false;
10274b4d4a4SAndreas Gohr        }
10374b4d4a4SAndreas Gohr
10474b4d4a4SAndreas Gohr        $userdata = $session->getUser();
10531039e80SAndreas Gohr        if (!$userdata) return false;
10604a78b87SAndreas Gohr        if (!isset($userdata['user'])) return false; // default dokuwiki does not put username here, let DW handle it
10774b4d4a4SAndreas Gohr        $session->setUser($userdata, false); // does a login without resetting the time
10874b4d4a4SAndreas Gohr        return true;
10974b4d4a4SAndreas Gohr    }
11074b4d4a4SAndreas Gohr
11174b4d4a4SAndreas Gohr    /**
1126d9a8a49SAndreas Gohr     * Login based on user cookie and a previously saved access token
11374b4d4a4SAndreas Gohr     *
1146d9a8a49SAndreas Gohr     * @return bool true if successful, false if not applies
115c82ad624SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
11674b4d4a4SAndreas Gohr     */
11774b4d4a4SAndreas Gohr    protected function loginByCookie()
11874b4d4a4SAndreas Gohr    {
11974b4d4a4SAndreas Gohr        $session = Session::getInstance();
12074b4d4a4SAndreas Gohr        $cookie = $session->getCookie();
12174b4d4a4SAndreas Gohr        if (!$cookie) return false;
12274b4d4a4SAndreas Gohr
12374b4d4a4SAndreas Gohr        $service = $this->loadService($cookie['servicename']);
12428002081SAndreas Gohr        $service->initOAuthService($cookie['storageId']);
125c82ad624SAndreas Gohr
126c82ad624SAndreas Gohr        // ensure that we have a current access token
1279cbef4d7SAndreas Gohr        $service->refreshOutdatedToken();
12874b4d4a4SAndreas Gohr
1296d9a8a49SAndreas Gohr        // this should use a previously saved token
1306d9a8a49SAndreas Gohr        $userdata = $service->getUser();
1316d9a8a49SAndreas Gohr
1326d9a8a49SAndreas Gohr        // processing
1336d9a8a49SAndreas Gohr        $userdata = $this->validateUserData($userdata, $cookie['servicename']);
1346d9a8a49SAndreas Gohr        $userdata = $this->processUserData($userdata, $cookie['servicename']);
1356d9a8a49SAndreas Gohr
13674b4d4a4SAndreas Gohr        $session->setUser($userdata); // log in
13774b4d4a4SAndreas Gohr        return true;
13874b4d4a4SAndreas Gohr    }
13974b4d4a4SAndreas Gohr
140a1fa007aSNaoto Kobayashi    /**
141a1fa007aSNaoto Kobayashi     * Callback service's logout
142a1fa007aSNaoto Kobayashi     *
143a1fa007aSNaoto Kobayashi     * @return void
144a1fa007aSNaoto Kobayashi     */
145a1fa007aSNaoto Kobayashi    public function logout()
146a1fa007aSNaoto Kobayashi    {
147a1fa007aSNaoto Kobayashi        $session = Session::getInstance();
148a1fa007aSNaoto Kobayashi        $cookie = $session->getCookie();
149a1fa007aSNaoto Kobayashi        if (!$cookie) return;
150a1fa007aSNaoto Kobayashi        try {
151a1fa007aSNaoto Kobayashi            $service = $this->loadService($cookie['servicename']);
152a1fa007aSNaoto Kobayashi            $service->initOAuthService($cookie['storageId']);
153a1fa007aSNaoto Kobayashi            $service->logout();
154a1fa007aSNaoto Kobayashi        } catch (\OAuth\Common\Exception\Exception $e) {
155a1fa007aSNaoto Kobayashi            return;
156a1fa007aSNaoto Kobayashi        }
157a1fa007aSNaoto Kobayashi    }
158a1fa007aSNaoto Kobayashi
1596d9a8a49SAndreas Gohr    // endregion
1606d9a8a49SAndreas Gohr
16174b4d4a4SAndreas Gohr    /**
16228002081SAndreas Gohr     * The ID we store authentication data as
16328002081SAndreas Gohr     *
16428002081SAndreas Gohr     * @param string $mail
16528002081SAndreas Gohr     * @return string
16628002081SAndreas Gohr     */
16728002081SAndreas Gohr    protected function getStorageId($mail)
16828002081SAndreas Gohr    {
16928002081SAndreas Gohr        return md5($mail);
17028002081SAndreas Gohr    }
17128002081SAndreas Gohr
17228002081SAndreas Gohr    /**
17374b4d4a4SAndreas Gohr     * Clean and validate the user data provided from the service
17474b4d4a4SAndreas Gohr     *
17574b4d4a4SAndreas Gohr     * @param array $userdata
17674b4d4a4SAndreas Gohr     * @param string $servicename
17774b4d4a4SAndreas Gohr     * @return array
17874b4d4a4SAndreas Gohr     * @throws Exception
17974b4d4a4SAndreas Gohr     */
18074b4d4a4SAndreas Gohr    protected function validateUserData($userdata, $servicename)
18174b4d4a4SAndreas Gohr    {
18274b4d4a4SAndreas Gohr        /** @var \auth_plugin_oauth */
18374b4d4a4SAndreas Gohr        global $auth;
18474b4d4a4SAndreas Gohr
18574b4d4a4SAndreas Gohr        // mail is required
18674b4d4a4SAndreas Gohr        if (empty($userdata['mail'])) {
187d1826331SAndreas Gohr            throw new Exception('noEmail', [$servicename]);
18874b4d4a4SAndreas Gohr        }
18974b4d4a4SAndreas Gohr
190e261c7e8SAndreas Gohr        $userdata['mail'] = strtolower($userdata['mail']);
191e261c7e8SAndreas Gohr
19274b4d4a4SAndreas Gohr        // mail needs to be allowed
19374b4d4a4SAndreas Gohr        /** @var \helper_plugin_oauth $hlp */
19474b4d4a4SAndreas Gohr        $hlp = plugin_load('helper', 'oauth');
19539730c7eSAnna Dabrowska
19639730c7eSAnna Dabrowska        if (!$hlp->checkMail($userdata['mail'])) {
197d1826331SAndreas Gohr            throw new Exception('rejectedEMail', [join(', ', $hlp->getValidDomains())]);
19839730c7eSAnna Dabrowska        }
19974b4d4a4SAndreas Gohr
20074b4d4a4SAndreas Gohr        // make username from mail if empty
2011a5ede3eSAndreas Gohr        if (!isset($userdata['user'])) $userdata['user'] = '';
20274b4d4a4SAndreas Gohr        $userdata['user'] = $auth->cleanUser((string)$userdata['user']);
2031a5ede3eSAndreas Gohr        if ($userdata['user'] === '') {
20474b4d4a4SAndreas Gohr            list($userdata['user']) = explode('@', $userdata['mail']);
20574b4d4a4SAndreas Gohr        }
20674b4d4a4SAndreas Gohr
20774b4d4a4SAndreas Gohr        // make full name from username if empty
20874b4d4a4SAndreas Gohr        if (empty($userdata['name'])) {
20974b4d4a4SAndreas Gohr            $userdata['name'] = $userdata['user'];
21074b4d4a4SAndreas Gohr        }
21174b4d4a4SAndreas Gohr
2121a5ede3eSAndreas Gohr        // make sure groups are array and valid
2131a5ede3eSAndreas Gohr        if (!isset($userdata['grps'])) $userdata['grps'] = [];
2141a5ede3eSAndreas Gohr        $userdata['grps'] = array_map([$auth, 'cleanGroup'], (array)$userdata['grps']);
2151a5ede3eSAndreas Gohr
21674b4d4a4SAndreas Gohr        return $userdata;
21774b4d4a4SAndreas Gohr    }
21874b4d4a4SAndreas Gohr
21974b4d4a4SAndreas Gohr    /**
22074b4d4a4SAndreas Gohr     * Process the userdata, update the user info array and create the user if necessary
22174b4d4a4SAndreas Gohr     *
22274b4d4a4SAndreas Gohr     * Uses the global $auth object for user management
22374b4d4a4SAndreas Gohr     *
22474b4d4a4SAndreas Gohr     * @param array $userdata User info received from authentication
22574b4d4a4SAndreas Gohr     * @param string $servicename Auth service
22674b4d4a4SAndreas Gohr     * @return array the modified user info
22774b4d4a4SAndreas Gohr     * @throws Exception
22874b4d4a4SAndreas Gohr     */
22974b4d4a4SAndreas Gohr    protected function processUserData($userdata, $servicename)
23074b4d4a4SAndreas Gohr    {
231e170f465SAndreas Gohr        /** @var \auth_plugin_oauth $auth */
23274b4d4a4SAndreas Gohr        global $auth;
23374b4d4a4SAndreas Gohr
23474b4d4a4SAndreas Gohr        // see if the user is known already
23574b4d4a4SAndreas Gohr        $localUser = $auth->getUserByEmail($userdata['mail']);
23674b4d4a4SAndreas Gohr        if ($localUser) {
23774b4d4a4SAndreas Gohr            $localUserInfo = $auth->getUserData($localUser);
238*d209a58cSAndreas Gohr            $localUserInfo['user'] = $localUser;
239*d209a58cSAndreas Gohr            if(isset($localUserInfo['pass'])) unset($localUserInfo['pass']);
240*d209a58cSAndreas Gohr
24174b4d4a4SAndreas Gohr            // check if the user allowed access via this service
24274b4d4a4SAndreas Gohr            if (!in_array($auth->cleanGroup($servicename), $localUserInfo['grps'])) {
243d1826331SAndreas Gohr                throw new Exception('authnotenabled', [$servicename]);
24474b4d4a4SAndreas Gohr            }
245f81e58d4SAndreas Gohr
246f81e58d4SAndreas Gohr            $helper = plugin_load('helper', 'oauth');
247f81e58d4SAndreas Gohr
24874b4d4a4SAndreas Gohr            $userdata['user'] = $localUser;
24974b4d4a4SAndreas Gohr            $userdata['name'] = $localUserInfo['name'];
250f81e58d4SAndreas Gohr            $userdata['grps'] = $this->mergeGroups(
251f81e58d4SAndreas Gohr                $localUserInfo['grps'],
252f81e58d4SAndreas Gohr                $userdata['grps'] ?? [],
253f81e58d4SAndreas Gohr                array_keys($helper->listServices(false)),
254f81e58d4SAndreas Gohr                $auth->getConf('overwrite-groups')
255f81e58d4SAndreas Gohr            );
2561e4efa57SAnna Dabrowska
257*d209a58cSAndreas Gohr            // update user if changed
258*d209a58cSAndreas Gohr            array_multisort($localUserInfo);
259*d209a58cSAndreas Gohr            array_multisort($userdata);
260*d209a58cSAndreas Gohr            if($localUserInfo != $userdata) {
2611e4efa57SAnna Dabrowska                $auth->modifyUser($localUser, $userdata);
262*d209a58cSAndreas Gohr            }
26374b4d4a4SAndreas Gohr        } elseif (actionOK('register') || $auth->getConf('register-on-auth')) {
264e170f465SAndreas Gohr            if (!$auth->registerOAuthUser($userdata, $servicename)) {
265d1826331SAndreas Gohr                throw new Exception('generic create error');
26674b4d4a4SAndreas Gohr            }
26774b4d4a4SAndreas Gohr        } else {
268d1826331SAndreas Gohr            throw new Exception('addUser not possible');
26974b4d4a4SAndreas Gohr        }
27074b4d4a4SAndreas Gohr
27174b4d4a4SAndreas Gohr        return $userdata;
27274b4d4a4SAndreas Gohr    }
27374b4d4a4SAndreas Gohr
27474b4d4a4SAndreas Gohr    /**
275ad56356cSAnna Dabrowska     * Merges local and provider user groups. Keeps internal
276ad56356cSAnna Dabrowska     * Dokuwiki groups unless configured to overwrite all ('overwrite-groups' setting)
277ad56356cSAnna Dabrowska     *
278f81e58d4SAndreas Gohr     * @param string[] $localGroups Local user groups
279f81e58d4SAndreas Gohr     * @param string[] $providerGroups Groups fetched from the provider
280f81e58d4SAndreas Gohr     * @param string[] $servicenames Service names that should be kept if set
281ad56356cSAnna Dabrowska     * @param bool $overwrite Config setting to overwrite local DokuWiki groups
282ad56356cSAnna Dabrowska     *
283ad56356cSAnna Dabrowska     * @return array
284ad56356cSAnna Dabrowska     */
285f81e58d4SAndreas Gohr    protected function mergeGroups($localGroups, $providerGroups, $servicenames, $overwrite)
286ad56356cSAnna Dabrowska    {
287ad56356cSAnna Dabrowska        global $conf;
288ad56356cSAnna Dabrowska
289f81e58d4SAndreas Gohr        // overwrite-groups set in config - remove all local groups except services and default
290f81e58d4SAndreas Gohr        if ($overwrite) {
291f81e58d4SAndreas Gohr            $localGroups = array_intersect($localGroups, array_merge($servicenames, [$conf['defaultgroup']]));
292f81e58d4SAndreas Gohr        }
293f81e58d4SAndreas Gohr
294f81e58d4SAndreas Gohr        return array_unique(array_merge($localGroups, $providerGroups));
295ad56356cSAnna Dabrowska    }
296ad56356cSAnna Dabrowska
297ad56356cSAnna Dabrowska    /**
29874b4d4a4SAndreas Gohr     * Instantiates a Service by name
29974b4d4a4SAndreas Gohr     *
30074b4d4a4SAndreas Gohr     * @param string $servicename
30104a78b87SAndreas Gohr     * @return Adapter
30274b4d4a4SAndreas Gohr     * @throws Exception
30374b4d4a4SAndreas Gohr     */
30474b4d4a4SAndreas Gohr    protected function loadService($servicename)
30574b4d4a4SAndreas Gohr    {
30674b4d4a4SAndreas Gohr        /** @var \helper_plugin_oauth $hlp */
30774b4d4a4SAndreas Gohr        $hlp = plugin_load('helper', 'oauth');
30874b4d4a4SAndreas Gohr        $srv = $hlp->loadService($servicename);
30974b4d4a4SAndreas Gohr
31074b4d4a4SAndreas Gohr        if ($srv === null) throw new Exception("No such service $servicename");
31174b4d4a4SAndreas Gohr        return $srv;
31274b4d4a4SAndreas Gohr    }
31374b4d4a4SAndreas Gohr
31474b4d4a4SAndreas Gohr}
315