xref: /plugin/oauth/OAuthManager.php (revision bfebac181e3dfee931da55d978e6f1a7b9df911c)
174b4d4a4SAndreas Gohr<?php
274b4d4a4SAndreas Gohr
374b4d4a4SAndreas Gohrnamespace dokuwiki\plugin\oauth;
474b4d4a4SAndreas Gohr
5*bfebac18SAndreas Gohruse dokuwiki\Logger;
6*bfebac18SAndreas Gohr
76d9a8a49SAndreas Gohr/**
86d9a8a49SAndreas Gohr * Implements the flow control for oAuth
96d9a8a49SAndreas Gohr */
1074b4d4a4SAndreas Gohrclass OAuthManager
1174b4d4a4SAndreas Gohr{
126d9a8a49SAndreas Gohr    // region main flow
1374b4d4a4SAndreas Gohr
1474b4d4a4SAndreas Gohr    /**
1531039e80SAndreas Gohr     * Explicitly starts the oauth flow by redirecting to IDP
1631039e80SAndreas Gohr     *
1704a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
1874b4d4a4SAndreas Gohr     */
1974b4d4a4SAndreas Gohr    public function startFlow($servicename)
2074b4d4a4SAndreas Gohr    {
2131039e80SAndreas Gohr        global $ID;
2231039e80SAndreas Gohr
2374b4d4a4SAndreas Gohr        $session = Session::getInstance();
2428002081SAndreas Gohr        $session->setLoginData($servicename, $ID);
2574b4d4a4SAndreas Gohr        $service = $this->loadService($servicename);
2628002081SAndreas Gohr        $service->initOAuthService();
2774b4d4a4SAndreas Gohr        $service->login(); // redirects
2874b4d4a4SAndreas Gohr    }
2974b4d4a4SAndreas Gohr
3074b4d4a4SAndreas Gohr    /**
3131039e80SAndreas Gohr     * Continues the flow from various states
3231039e80SAndreas Gohr     *
3374b4d4a4SAndreas Gohr     * @return bool true if the login has been handled
3474b4d4a4SAndreas Gohr     * @throws Exception
3574b4d4a4SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
3674b4d4a4SAndreas Gohr     */
3774b4d4a4SAndreas Gohr    public function continueFlow()
3874b4d4a4SAndreas Gohr    {
3974b4d4a4SAndreas Gohr        return $this->loginByService() or
4074b4d4a4SAndreas Gohr            $this->loginBySession() or
4174b4d4a4SAndreas Gohr            $this->loginByCookie();
4274b4d4a4SAndreas Gohr    }
4374b4d4a4SAndreas Gohr
4474b4d4a4SAndreas Gohr    /**
456d9a8a49SAndreas Gohr     * Second step in a explicit login, validates the oauth code
466d9a8a49SAndreas Gohr     *
476d9a8a49SAndreas Gohr     * @return bool true if successful, false if not applies
4874b4d4a4SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
4974b4d4a4SAndreas Gohr     */
5074b4d4a4SAndreas Gohr    protected function loginByService()
5174b4d4a4SAndreas Gohr    {
5274b4d4a4SAndreas Gohr        global $INPUT;
5374b4d4a4SAndreas Gohr
5474b4d4a4SAndreas Gohr        if (!$INPUT->get->has('code') && !$INPUT->get->has('oauth_token')) {
5574b4d4a4SAndreas Gohr            return false;
5674b4d4a4SAndreas Gohr        }
5774b4d4a4SAndreas Gohr
5874b4d4a4SAndreas Gohr        $session = Session::getInstance();
5974b4d4a4SAndreas Gohr
6074b4d4a4SAndreas Gohr        // init service from session
6174b4d4a4SAndreas Gohr        $logindata = $session->getLoginData();
6274b4d4a4SAndreas Gohr        if (!$logindata) return false;
6374b4d4a4SAndreas Gohr        $service = $this->loadService($logindata['servicename']);
6428002081SAndreas Gohr        $service->initOAuthService();
6574b4d4a4SAndreas Gohr        $session->clearLoginData();
6674b4d4a4SAndreas Gohr
6774b4d4a4SAndreas Gohr        // oAuth login
6804a78b87SAndreas Gohr        if (!$service->checkToken()) throw new \OAuth\Common\Exception\Exception("Invalid Token - Login failed");
6974b4d4a4SAndreas Gohr        $userdata = $service->getUser();
7074b4d4a4SAndreas Gohr
7174b4d4a4SAndreas Gohr        // processing
7274b4d4a4SAndreas Gohr        $userdata = $this->validateUserData($userdata, $logindata['servicename']);
7374b4d4a4SAndreas Gohr        $userdata = $this->processUserData($userdata, $logindata['servicename']);
7474b4d4a4SAndreas Gohr
7528002081SAndreas Gohr        // store data
7628002081SAndreas Gohr        $storageId = $this->getStorageId($userdata['mail']);
7728002081SAndreas Gohr        $service->upgradeStorage($storageId);
7828002081SAndreas Gohr
7974b4d4a4SAndreas Gohr        // login
8074b4d4a4SAndreas Gohr        $session->setUser($userdata); // log in
8128002081SAndreas Gohr        $session->setCookie($logindata['servicename'], $storageId); // set cookie
8274b4d4a4SAndreas Gohr
8331039e80SAndreas Gohr        // redirect to the appropriate ID
8431039e80SAndreas Gohr        if (!empty($logindata['id'])) {
8531039e80SAndreas Gohr            send_redirect(wl($logindata['id'], [], true, '&'));
8631039e80SAndreas Gohr        }
8774b4d4a4SAndreas Gohr        return true;
8874b4d4a4SAndreas Gohr    }
8974b4d4a4SAndreas Gohr
9074b4d4a4SAndreas Gohr    /**
916d9a8a49SAndreas Gohr     * Login based on user's current session data
9274b4d4a4SAndreas Gohr     *
9374b4d4a4SAndreas Gohr     * This will also log in plainauth users
9474b4d4a4SAndreas Gohr     *
956d9a8a49SAndreas Gohr     * @return bool true if successful, false if not applies
9674b4d4a4SAndreas Gohr     * @throws Exception
9774b4d4a4SAndreas Gohr     */
9874b4d4a4SAndreas Gohr    protected function loginBySession()
9974b4d4a4SAndreas Gohr    {
10074b4d4a4SAndreas Gohr        $session = Session::getInstance();
10174b4d4a4SAndreas Gohr        if (!$session->isValid()) {
10274b4d4a4SAndreas Gohr            $session->clear();
10374b4d4a4SAndreas Gohr            return false;
10474b4d4a4SAndreas Gohr        }
10574b4d4a4SAndreas Gohr
10674b4d4a4SAndreas Gohr        $userdata = $session->getUser();
10731039e80SAndreas Gohr        if (!$userdata) return false;
10804a78b87SAndreas Gohr        if (!isset($userdata['user'])) return false; // default dokuwiki does not put username here, let DW handle it
10974b4d4a4SAndreas Gohr        $session->setUser($userdata, false); // does a login without resetting the time
11074b4d4a4SAndreas Gohr        return true;
11174b4d4a4SAndreas Gohr    }
11274b4d4a4SAndreas Gohr
11374b4d4a4SAndreas Gohr    /**
1146d9a8a49SAndreas Gohr     * Login based on user cookie and a previously saved access token
11574b4d4a4SAndreas Gohr     *
1166d9a8a49SAndreas Gohr     * @return bool true if successful, false if not applies
117c82ad624SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
11874b4d4a4SAndreas Gohr     */
11974b4d4a4SAndreas Gohr    protected function loginByCookie()
12074b4d4a4SAndreas Gohr    {
12174b4d4a4SAndreas Gohr        $session = Session::getInstance();
12274b4d4a4SAndreas Gohr        $cookie = $session->getCookie();
12374b4d4a4SAndreas Gohr        if (!$cookie) return false;
12474b4d4a4SAndreas Gohr
12574b4d4a4SAndreas Gohr        $service = $this->loadService($cookie['servicename']);
12628002081SAndreas Gohr        $service->initOAuthService($cookie['storageId']);
127c82ad624SAndreas Gohr
128c82ad624SAndreas Gohr        // ensure that we have a current access token
1299cbef4d7SAndreas Gohr        $service->refreshOutdatedToken();
13074b4d4a4SAndreas Gohr
1316d9a8a49SAndreas Gohr        // this should use a previously saved token
1326d9a8a49SAndreas Gohr        $userdata = $service->getUser();
1336d9a8a49SAndreas Gohr
1346d9a8a49SAndreas Gohr        // processing
1356d9a8a49SAndreas Gohr        $userdata = $this->validateUserData($userdata, $cookie['servicename']);
1366d9a8a49SAndreas Gohr        $userdata = $this->processUserData($userdata, $cookie['servicename']);
1376d9a8a49SAndreas Gohr
13874b4d4a4SAndreas Gohr        $session->setUser($userdata); // log in
13974b4d4a4SAndreas Gohr        return true;
14074b4d4a4SAndreas Gohr    }
14174b4d4a4SAndreas Gohr
142a1fa007aSNaoto Kobayashi    /**
143a1fa007aSNaoto Kobayashi     * Callback service's logout
144a1fa007aSNaoto Kobayashi     *
145a1fa007aSNaoto Kobayashi     * @return void
146a1fa007aSNaoto Kobayashi     */
147a1fa007aSNaoto Kobayashi    public function logout()
148a1fa007aSNaoto Kobayashi    {
149a1fa007aSNaoto Kobayashi        $session = Session::getInstance();
150a1fa007aSNaoto Kobayashi        $cookie = $session->getCookie();
151a1fa007aSNaoto Kobayashi        if (!$cookie) return;
152a1fa007aSNaoto Kobayashi        try {
153a1fa007aSNaoto Kobayashi            $service = $this->loadService($cookie['servicename']);
154a1fa007aSNaoto Kobayashi            $service->initOAuthService($cookie['storageId']);
155a1fa007aSNaoto Kobayashi            $service->logout();
156a1fa007aSNaoto Kobayashi        } catch (\OAuth\Common\Exception\Exception $e) {
157a1fa007aSNaoto Kobayashi            return;
158a1fa007aSNaoto Kobayashi        }
159a1fa007aSNaoto Kobayashi    }
160a1fa007aSNaoto Kobayashi
1616d9a8a49SAndreas Gohr    // endregion
1626d9a8a49SAndreas Gohr
16374b4d4a4SAndreas Gohr    /**
16428002081SAndreas Gohr     * The ID we store authentication data as
16528002081SAndreas Gohr     *
16628002081SAndreas Gohr     * @param string $mail
16728002081SAndreas Gohr     * @return string
16828002081SAndreas Gohr     */
16928002081SAndreas Gohr    protected function getStorageId($mail)
17028002081SAndreas Gohr    {
17128002081SAndreas Gohr        return md5($mail);
17228002081SAndreas Gohr    }
17328002081SAndreas Gohr
17428002081SAndreas Gohr    /**
17574b4d4a4SAndreas Gohr     * Clean and validate the user data provided from the service
17674b4d4a4SAndreas Gohr     *
17774b4d4a4SAndreas Gohr     * @param array $userdata
17874b4d4a4SAndreas Gohr     * @param string $servicename
17974b4d4a4SAndreas Gohr     * @return array
18074b4d4a4SAndreas Gohr     * @throws Exception
18174b4d4a4SAndreas Gohr     */
18274b4d4a4SAndreas Gohr    protected function validateUserData($userdata, $servicename)
18374b4d4a4SAndreas Gohr    {
18474b4d4a4SAndreas Gohr        /** @var \auth_plugin_oauth */
18574b4d4a4SAndreas Gohr        global $auth;
18674b4d4a4SAndreas Gohr
18774b4d4a4SAndreas Gohr        // mail is required
18874b4d4a4SAndreas Gohr        if (empty($userdata['mail'])) {
189d1826331SAndreas Gohr            throw new Exception('noEmail', [$servicename]);
19074b4d4a4SAndreas Gohr        }
19174b4d4a4SAndreas Gohr
192e261c7e8SAndreas Gohr        $userdata['mail'] = strtolower($userdata['mail']);
193e261c7e8SAndreas Gohr
19474b4d4a4SAndreas Gohr        // mail needs to be allowed
19574b4d4a4SAndreas Gohr        /** @var \helper_plugin_oauth $hlp */
19674b4d4a4SAndreas Gohr        $hlp = plugin_load('helper', 'oauth');
19739730c7eSAnna Dabrowska
19839730c7eSAnna Dabrowska        if (!$hlp->checkMail($userdata['mail'])) {
199d1826331SAndreas Gohr            throw new Exception('rejectedEMail', [join(', ', $hlp->getValidDomains())]);
20039730c7eSAnna Dabrowska        }
20174b4d4a4SAndreas Gohr
20274b4d4a4SAndreas Gohr        // make username from mail if empty
2031a5ede3eSAndreas Gohr        if (!isset($userdata['user'])) $userdata['user'] = '';
20474b4d4a4SAndreas Gohr        $userdata['user'] = $auth->cleanUser((string)$userdata['user']);
2051a5ede3eSAndreas Gohr        if ($userdata['user'] === '') {
20674b4d4a4SAndreas Gohr            list($userdata['user']) = explode('@', $userdata['mail']);
20774b4d4a4SAndreas Gohr        }
20874b4d4a4SAndreas Gohr
20974b4d4a4SAndreas Gohr        // make full name from username if empty
21074b4d4a4SAndreas Gohr        if (empty($userdata['name'])) {
21174b4d4a4SAndreas Gohr            $userdata['name'] = $userdata['user'];
21274b4d4a4SAndreas Gohr        }
21374b4d4a4SAndreas Gohr
2141a5ede3eSAndreas Gohr        // make sure groups are array and valid
2151a5ede3eSAndreas Gohr        if (!isset($userdata['grps'])) $userdata['grps'] = [];
2161a5ede3eSAndreas Gohr        $userdata['grps'] = array_map([$auth, 'cleanGroup'], (array)$userdata['grps']);
2171a5ede3eSAndreas Gohr
21874b4d4a4SAndreas Gohr        return $userdata;
21974b4d4a4SAndreas Gohr    }
22074b4d4a4SAndreas Gohr
22174b4d4a4SAndreas Gohr    /**
22274b4d4a4SAndreas Gohr     * Process the userdata, update the user info array and create the user if necessary
22374b4d4a4SAndreas Gohr     *
22474b4d4a4SAndreas Gohr     * Uses the global $auth object for user management
22574b4d4a4SAndreas Gohr     *
22674b4d4a4SAndreas Gohr     * @param array $userdata User info received from authentication
22774b4d4a4SAndreas Gohr     * @param string $servicename Auth service
22874b4d4a4SAndreas Gohr     * @return array the modified user info
22974b4d4a4SAndreas Gohr     * @throws Exception
23074b4d4a4SAndreas Gohr     */
23174b4d4a4SAndreas Gohr    protected function processUserData($userdata, $servicename)
23274b4d4a4SAndreas Gohr    {
233e170f465SAndreas Gohr        /** @var \auth_plugin_oauth $auth */
23474b4d4a4SAndreas Gohr        global $auth;
23574b4d4a4SAndreas Gohr
23674b4d4a4SAndreas Gohr        // see if the user is known already
23774b4d4a4SAndreas Gohr        $localUser = $auth->getUserByEmail($userdata['mail']);
23874b4d4a4SAndreas Gohr        if ($localUser) {
23974b4d4a4SAndreas Gohr            $localUserInfo = $auth->getUserData($localUser);
240d209a58cSAndreas Gohr            $localUserInfo['user'] = $localUser;
241d209a58cSAndreas Gohr            if(isset($localUserInfo['pass'])) unset($localUserInfo['pass']);
242d209a58cSAndreas Gohr
24374b4d4a4SAndreas Gohr            // check if the user allowed access via this service
24474b4d4a4SAndreas Gohr            if (!in_array($auth->cleanGroup($servicename), $localUserInfo['grps'])) {
245d1826331SAndreas Gohr                throw new Exception('authnotenabled', [$servicename]);
24674b4d4a4SAndreas Gohr            }
247f81e58d4SAndreas Gohr
248f81e58d4SAndreas Gohr            $helper = plugin_load('helper', 'oauth');
249f81e58d4SAndreas Gohr
25074b4d4a4SAndreas Gohr            $userdata['user'] = $localUser;
25174b4d4a4SAndreas Gohr            $userdata['name'] = $localUserInfo['name'];
252f81e58d4SAndreas Gohr            $userdata['grps'] = $this->mergeGroups(
253f81e58d4SAndreas Gohr                $localUserInfo['grps'],
254f81e58d4SAndreas Gohr                $userdata['grps'] ?? [],
255f81e58d4SAndreas Gohr                array_keys($helper->listServices(false)),
256f81e58d4SAndreas Gohr                $auth->getConf('overwrite-groups')
257f81e58d4SAndreas Gohr            );
2581e4efa57SAnna Dabrowska
259d209a58cSAndreas Gohr            // update user if changed
260*bfebac18SAndreas Gohr            sort($localUserInfo['grps']);
261*bfebac18SAndreas Gohr            sort($userdata['grps']);
262*bfebac18SAndreas Gohr            if ($localUserInfo != $userdata && !isset($localUserInfo['protected'])) {
2631e4efa57SAnna Dabrowska                $auth->modifyUser($localUser, $userdata);
264d209a58cSAndreas Gohr            }
26574b4d4a4SAndreas Gohr        } elseif (actionOK('register') || $auth->getConf('register-on-auth')) {
266e170f465SAndreas Gohr            if (!$auth->registerOAuthUser($userdata, $servicename)) {
267d1826331SAndreas Gohr                throw new Exception('generic create error');
26874b4d4a4SAndreas Gohr            }
26974b4d4a4SAndreas Gohr        } else {
270d1826331SAndreas Gohr            throw new Exception('addUser not possible');
27174b4d4a4SAndreas Gohr        }
27274b4d4a4SAndreas Gohr
27374b4d4a4SAndreas Gohr        return $userdata;
27474b4d4a4SAndreas Gohr    }
27574b4d4a4SAndreas Gohr
27674b4d4a4SAndreas Gohr    /**
277ad56356cSAnna Dabrowska     * Merges local and provider user groups. Keeps internal
278ad56356cSAnna Dabrowska     * Dokuwiki groups unless configured to overwrite all ('overwrite-groups' setting)
279ad56356cSAnna Dabrowska     *
280f81e58d4SAndreas Gohr     * @param string[] $localGroups Local user groups
281f81e58d4SAndreas Gohr     * @param string[] $providerGroups Groups fetched from the provider
282f81e58d4SAndreas Gohr     * @param string[] $servicenames Service names that should be kept if set
283ad56356cSAnna Dabrowska     * @param bool $overwrite Config setting to overwrite local DokuWiki groups
284ad56356cSAnna Dabrowska     *
285ad56356cSAnna Dabrowska     * @return array
286ad56356cSAnna Dabrowska     */
287f81e58d4SAndreas Gohr    protected function mergeGroups($localGroups, $providerGroups, $servicenames, $overwrite)
288ad56356cSAnna Dabrowska    {
289ad56356cSAnna Dabrowska        global $conf;
290ad56356cSAnna Dabrowska
291f81e58d4SAndreas Gohr        // overwrite-groups set in config - remove all local groups except services and default
292f81e58d4SAndreas Gohr        if ($overwrite) {
293f81e58d4SAndreas Gohr            $localGroups = array_intersect($localGroups, array_merge($servicenames, [$conf['defaultgroup']]));
294f81e58d4SAndreas Gohr        }
295f81e58d4SAndreas Gohr
296f81e58d4SAndreas Gohr        return array_unique(array_merge($localGroups, $providerGroups));
297ad56356cSAnna Dabrowska    }
298ad56356cSAnna Dabrowska
299ad56356cSAnna Dabrowska    /**
30074b4d4a4SAndreas Gohr     * Instantiates a Service by name
30174b4d4a4SAndreas Gohr     *
30274b4d4a4SAndreas Gohr     * @param string $servicename
30304a78b87SAndreas Gohr     * @return Adapter
30474b4d4a4SAndreas Gohr     * @throws Exception
30574b4d4a4SAndreas Gohr     */
30674b4d4a4SAndreas Gohr    protected function loadService($servicename)
30774b4d4a4SAndreas Gohr    {
30874b4d4a4SAndreas Gohr        /** @var \helper_plugin_oauth $hlp */
30974b4d4a4SAndreas Gohr        $hlp = plugin_load('helper', 'oauth');
31074b4d4a4SAndreas Gohr        $srv = $hlp->loadService($servicename);
31174b4d4a4SAndreas Gohr
31274b4d4a4SAndreas Gohr        if ($srv === null) throw new Exception("No such service $servicename");
31374b4d4a4SAndreas Gohr        return $srv;
31474b4d4a4SAndreas Gohr    }
31574b4d4a4SAndreas Gohr
31674b4d4a4SAndreas Gohr}
317