xref: /plugin/oauth/Adapter.php (revision 9cbef4d7b9ae5efb4aae394eacab1875af2b2307)
104a78b87SAndreas Gohr<?php
204a78b87SAndreas Gohr
304a78b87SAndreas Gohrnamespace dokuwiki\plugin\oauth;
404a78b87SAndreas Gohr
504a78b87SAndreas Gohruse dokuwiki\Extension\ActionPlugin;
604a78b87SAndreas Gohruse OAuth\Common\Consumer\Credentials;
704a78b87SAndreas Gohruse OAuth\Common\Http\Exception\TokenResponseException;
8*9cbef4d7SAndreas Gohruse OAuth\Common\Storage\Exception\TokenNotFoundException;
904a78b87SAndreas Gohruse OAuth\OAuth1\Service\AbstractService as Abstract1Service;
1004a78b87SAndreas Gohruse OAuth\OAuth1\Token\TokenInterface;
1104a78b87SAndreas Gohruse OAuth\OAuth2\Service\AbstractService as Abstract2Service;
1204a78b87SAndreas Gohruse OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException;
13*9cbef4d7SAndreas Gohruse OAuth\OAuth2\Service\Exception\MissingRefreshTokenException;
1404a78b87SAndreas Gohruse OAuth\ServiceFactory;
1504a78b87SAndreas Gohr
1604a78b87SAndreas Gohr/**
1704a78b87SAndreas Gohr * Base class to implement a Backend Service for the oAuth Plugin
1804a78b87SAndreas Gohr */
1904a78b87SAndreas Gohrabstract class Adapter extends ActionPlugin
2004a78b87SAndreas Gohr{
2104a78b87SAndreas Gohr    /**
2204a78b87SAndreas Gohr     * @var Abstract2Service|Abstract1Service
2304a78b87SAndreas Gohr     * @see getOAuthService() use this to ensure it's intialized
2404a78b87SAndreas Gohr     */
2504a78b87SAndreas Gohr    protected $oAuth;
2604a78b87SAndreas Gohr
2704a78b87SAndreas Gohr    // region internal methods
2804a78b87SAndreas Gohr
2904a78b87SAndreas Gohr    /**
3004a78b87SAndreas Gohr     * Auto register this plugin with the oAuth authentication plugin
3104a78b87SAndreas Gohr     *
3204a78b87SAndreas Gohr     * @inheritDoc
3304a78b87SAndreas Gohr     */
3404a78b87SAndreas Gohr    public function register(\Doku_Event_Handler $controller)
3504a78b87SAndreas Gohr    {
3604a78b87SAndreas Gohr        $controller->register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister');
3704a78b87SAndreas Gohr    }
3804a78b87SAndreas Gohr
3904a78b87SAndreas Gohr    /**
4004a78b87SAndreas Gohr     * Auto register this plugin with the oAuth authentication plugin
4104a78b87SAndreas Gohr     */
4204a78b87SAndreas Gohr    public function handleRegister(\Doku_Event $event, $param)
4304a78b87SAndreas Gohr    {
4404a78b87SAndreas Gohr        $event->data[$this->getServiceID()] = $this;
4504a78b87SAndreas Gohr    }
4604a78b87SAndreas Gohr
4704a78b87SAndreas Gohr    /**
4804a78b87SAndreas Gohr     * Initialize the oAuth service
4904a78b87SAndreas Gohr     *
5004a78b87SAndreas Gohr     * @param string $guid UIID for the user to authenticate
5104a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
5204a78b87SAndreas Gohr     */
5304a78b87SAndreas Gohr    public function initOAuthService($guid)
5404a78b87SAndreas Gohr    {
5504a78b87SAndreas Gohr        /** @var \helper_plugin_oauth $hlp */
5604a78b87SAndreas Gohr        $hlp = plugin_load('helper', 'oauth');
5704a78b87SAndreas Gohr
5804a78b87SAndreas Gohr        $credentials = new Credentials(
5904a78b87SAndreas Gohr            $this->getKey(),
6004a78b87SAndreas Gohr            $this->getSecret(),
6104a78b87SAndreas Gohr            $hlp->redirectURI()
6204a78b87SAndreas Gohr        );
6304a78b87SAndreas Gohr
6404a78b87SAndreas Gohr        $serviceFactory = new ServiceFactory();
6504a78b87SAndreas Gohr        $serviceFactory->setHttpClient(new HTTPClient());
6604a78b87SAndreas Gohr        $servicename = $this->getServiceID();
6704a78b87SAndreas Gohr        $serviceclass = $this->registerServiceClass();
6804a78b87SAndreas Gohr        if ($serviceclass) {
6904a78b87SAndreas Gohr            $serviceFactory->registerService($servicename, $serviceclass);
7004a78b87SAndreas Gohr        }
7104a78b87SAndreas Gohr
7204a78b87SAndreas Gohr        $this->oAuth = $serviceFactory->createService(
7304a78b87SAndreas Gohr            $servicename,
7404a78b87SAndreas Gohr            $credentials,
7504a78b87SAndreas Gohr            new Storage($guid),
7604a78b87SAndreas Gohr            $this->getScopes()
7704a78b87SAndreas Gohr        );
7804a78b87SAndreas Gohr
7904a78b87SAndreas Gohr        if ($this->oAuth === null) {
8004a78b87SAndreas Gohr            throw new Exception('Failed to initialize Service ' . $this->getLabel());
8104a78b87SAndreas Gohr        }
8204a78b87SAndreas Gohr    }
8304a78b87SAndreas Gohr
8404a78b87SAndreas Gohr    /**
8504a78b87SAndreas Gohr     * @return Abstract2Service|Abstract1Service
8604a78b87SAndreas Gohr     * @throws Exception
8704a78b87SAndreas Gohr     */
8804a78b87SAndreas Gohr    public function getOAuthService()
8904a78b87SAndreas Gohr    {
9004a78b87SAndreas Gohr        if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized');
9104a78b87SAndreas Gohr        return $this->oAuth;
9204a78b87SAndreas Gohr    }
9304a78b87SAndreas Gohr
9404a78b87SAndreas Gohr    /**
95*9cbef4d7SAndreas Gohr     * Refresh a possibly outdated access token
96*9cbef4d7SAndreas Gohr     *
97*9cbef4d7SAndreas Gohr     * Does nothing when the current token is still good to use
98*9cbef4d7SAndreas Gohr     *
99*9cbef4d7SAndreas Gohr     * @return void
100*9cbef4d7SAndreas Gohr     * @throws MissingRefreshTokenException
101*9cbef4d7SAndreas Gohr     * @throws TokenNotFoundException
102*9cbef4d7SAndreas Gohr     * @throws TokenResponseException
103*9cbef4d7SAndreas Gohr     * @throws Exception
104*9cbef4d7SAndreas Gohr     */
105*9cbef4d7SAndreas Gohr    public function refreshOutdatedToken()
106*9cbef4d7SAndreas Gohr    {
107*9cbef4d7SAndreas Gohr        $oauth = $this->getOAuthService();
108*9cbef4d7SAndreas Gohr
109*9cbef4d7SAndreas Gohr        if (!$oauth->getStorage()->hasAccessToken($oauth->service())) {
110*9cbef4d7SAndreas Gohr            // no token to refresh
111*9cbef4d7SAndreas Gohr            return;
112*9cbef4d7SAndreas Gohr        }
113*9cbef4d7SAndreas Gohr
114*9cbef4d7SAndreas Gohr        $token = $oauth->getStorage()->retrieveAccessToken($oauth->service());
115*9cbef4d7SAndreas Gohr        if ($token->getEndOfLife() < 0 ||
116*9cbef4d7SAndreas Gohr            $token->getEndOfLife() - time() > 3600) {
117*9cbef4d7SAndreas Gohr            // token is still good
118*9cbef4d7SAndreas Gohr            return;
119*9cbef4d7SAndreas Gohr        }
120*9cbef4d7SAndreas Gohr
121*9cbef4d7SAndreas Gohr        $refreshToken = $token->getRefreshToken();
122*9cbef4d7SAndreas Gohr        $token = $oauth->refreshAccessToken($token);
123*9cbef4d7SAndreas Gohr
124*9cbef4d7SAndreas Gohr        // If the IDP did not provide a new refresh token, store the old one
125*9cbef4d7SAndreas Gohr        if (!$token->getRefreshToken()) {
126*9cbef4d7SAndreas Gohr            $token->setRefreshToken($refreshToken);
127*9cbef4d7SAndreas Gohr            $oauth->getStorage()->storeAccessToken($oauth->service(), $token);
128*9cbef4d7SAndreas Gohr        }
129*9cbef4d7SAndreas Gohr    }
130*9cbef4d7SAndreas Gohr
131*9cbef4d7SAndreas Gohr    /**
13204a78b87SAndreas Gohr     * Redirects to the service for requesting access
13304a78b87SAndreas Gohr     *
13404a78b87SAndreas Gohr     * This is the first step of oAuth authentication
13504a78b87SAndreas Gohr     *
13604a78b87SAndreas Gohr     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
13704a78b87SAndreas Gohr     * but might need to be overwritten for specific services
13804a78b87SAndreas Gohr     *
13904a78b87SAndreas Gohr     * @throws TokenResponseException
14004a78b87SAndreas Gohr     * @throws Exception
14104a78b87SAndreas Gohr     */
14204a78b87SAndreas Gohr    public function login()
14304a78b87SAndreas Gohr    {
14404a78b87SAndreas Gohr        $oauth = $this->getOAuthService();
14504a78b87SAndreas Gohr
14604a78b87SAndreas Gohr        // store Farmer animal in oAuth state parameter
14704a78b87SAndreas Gohr        /** @var \helper_plugin_farmer $farmer */
14804a78b87SAndreas Gohr        $farmer = plugin_load('helper', 'farmer');
14904a78b87SAndreas Gohr        $parameters = [];
15004a78b87SAndreas Gohr        if ($farmer && $animal = $farmer->getAnimal()) {
15104a78b87SAndreas Gohr            $parameters['state'] = urlencode(base64_encode(json_encode(
15204a78b87SAndreas Gohr                [
15304a78b87SAndreas Gohr                    'animal' => $animal,
15404a78b87SAndreas Gohr                    'state' => md5(rand()),
15504a78b87SAndreas Gohr                ]
15604a78b87SAndreas Gohr            )));
15704a78b87SAndreas Gohr            $oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']);
15804a78b87SAndreas Gohr        }
15904a78b87SAndreas Gohr
16004a78b87SAndreas Gohr        if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */
16104a78b87SAndreas Gohr            // extra request needed for oauth1 to request a request token
16204a78b87SAndreas Gohr            $token = $oauth->requestRequestToken();
16304a78b87SAndreas Gohr            $parameters['oauth_token'] = $token->getRequestToken();
16404a78b87SAndreas Gohr        }
16504a78b87SAndreas Gohr        $url = $oauth->getAuthorizationUri($parameters);
16604a78b87SAndreas Gohr
16704a78b87SAndreas Gohr        send_redirect($url);
16804a78b87SAndreas Gohr    }
16904a78b87SAndreas Gohr
17004a78b87SAndreas Gohr    /**
17104a78b87SAndreas Gohr     * Request access token
17204a78b87SAndreas Gohr     *
17304a78b87SAndreas Gohr     * This is the second step of oAuth authentication
17404a78b87SAndreas Gohr     *
17504a78b87SAndreas Gohr     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
17604a78b87SAndreas Gohr     * but might need to be overwritten for specific services
17704a78b87SAndreas Gohr     *
17804a78b87SAndreas Gohr     * Thrown exceptions indicate a non-successful login because of some error, appropriate messages
17904a78b87SAndreas Gohr     * should be shown to the user. A return of false with no exceptions indicates that there was no
18004a78b87SAndreas Gohr     * oauth data at all. This can probably be silently ignored.
18104a78b87SAndreas Gohr     *
18204a78b87SAndreas Gohr     * @return bool true if authentication was successful
18304a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
18404a78b87SAndreas Gohr     * @throws InvalidAuthorizationStateException
18504a78b87SAndreas Gohr     */
18604a78b87SAndreas Gohr    public function checkToken()
18704a78b87SAndreas Gohr    {
18804a78b87SAndreas Gohr        global $INPUT;
18904a78b87SAndreas Gohr
19004a78b87SAndreas Gohr        $oauth = $this->getOAuthService();
19104a78b87SAndreas Gohr
19204a78b87SAndreas Gohr        if (is_a($oauth, Abstract2Service::class)) {
19304a78b87SAndreas Gohr            /** @var Abstract2Service $oauth */
19404a78b87SAndreas Gohr            if (!$INPUT->get->has('code')) return false;
19504a78b87SAndreas Gohr            $state = $INPUT->get->str('state', null);
196eae50416SAndreas Gohr            $accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state);
19704a78b87SAndreas Gohr        } else {
19804a78b87SAndreas Gohr            /** @var Abstract1Service $oauth */
19904a78b87SAndreas Gohr            if (!$INPUT->get->has('oauth_token')) return false;
20004a78b87SAndreas Gohr            /** @var TokenInterface $token */
20104a78b87SAndreas Gohr            $token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID());
202eae50416SAndreas Gohr            $accessToken = $oauth->requestAccessToken(
20304a78b87SAndreas Gohr                $INPUT->get->str('oauth_token'),
20404a78b87SAndreas Gohr                $INPUT->get->str('oauth_verifier'),
20504a78b87SAndreas Gohr                $token->getRequestTokenSecret()
20604a78b87SAndreas Gohr            );
20704a78b87SAndreas Gohr        }
208eae50416SAndreas Gohr
209eae50416SAndreas Gohr        if (
210eae50416SAndreas Gohr            $accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES &&
211eae50416SAndreas Gohr            !$accessToken->getRefreshToken()) {
212eae50416SAndreas Gohr            msg('Service did not provide a Refresh Token. You will be logged out when the session expires.');
213eae50416SAndreas Gohr        }
214eae50416SAndreas Gohr
21504a78b87SAndreas Gohr        return true;
21604a78b87SAndreas Gohr    }
21704a78b87SAndreas Gohr
21804a78b87SAndreas Gohr    /**
21904a78b87SAndreas Gohr     * Return the Service Login Button
22004a78b87SAndreas Gohr     *
22104a78b87SAndreas Gohr     * @return string
22204a78b87SAndreas Gohr     */
22304a78b87SAndreas Gohr    public function loginButton()
22404a78b87SAndreas Gohr    {
22504a78b87SAndreas Gohr        global $ID;
22604a78b87SAndreas Gohr
22704a78b87SAndreas Gohr        $attr = buildAttributes([
22804a78b87SAndreas Gohr            'href' => wl($ID, array('oauthlogin' => $this->getServiceID()), false, '&'),
22904a78b87SAndreas Gohr            'class' => 'plugin_oauth_' . $this->getServiceID(),
23004a78b87SAndreas Gohr            'style' => 'background-color: ' . $this->getColor(),
23104a78b87SAndreas Gohr        ]);
23204a78b87SAndreas Gohr
23304a78b87SAndreas Gohr        return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> ';
23404a78b87SAndreas Gohr    }
23504a78b87SAndreas Gohr    // endregion
23604a78b87SAndreas Gohr
23704a78b87SAndreas Gohr    // region overridable methods
23804a78b87SAndreas Gohr
23904a78b87SAndreas Gohr    /**
24004a78b87SAndreas Gohr     * Retrieve the user's data via API
24104a78b87SAndreas Gohr     *
24204a78b87SAndreas Gohr     * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps'
24304a78b87SAndreas Gohr     *
24404a78b87SAndreas Gohr     * Use the request() method of the oauth object to talk to the API
24504a78b87SAndreas Gohr     *
24604a78b87SAndreas Gohr     * @return array
24704a78b87SAndreas Gohr     * @throws Exception
24804a78b87SAndreas Gohr     * @see getOAuthService()
24904a78b87SAndreas Gohr     */
25004a78b87SAndreas Gohr    abstract public function getUser();
25104a78b87SAndreas Gohr
25204a78b87SAndreas Gohr    /**
25304a78b87SAndreas Gohr     * Return the scopes to request
25404a78b87SAndreas Gohr     *
25504a78b87SAndreas Gohr     * This should return the minimal scopes needed for accessing the user's data
25604a78b87SAndreas Gohr     *
25704a78b87SAndreas Gohr     * @return string[]
25804a78b87SAndreas Gohr     */
25904a78b87SAndreas Gohr    public function getScopes()
26004a78b87SAndreas Gohr    {
26104a78b87SAndreas Gohr        return [];
26204a78b87SAndreas Gohr    }
26304a78b87SAndreas Gohr
26404a78b87SAndreas Gohr    /**
26504a78b87SAndreas Gohr     * Return the user friendly name of the service
26604a78b87SAndreas Gohr     *
26704a78b87SAndreas Gohr     * Defaults to ServiceID. You may want to override this.
26804a78b87SAndreas Gohr     *
26904a78b87SAndreas Gohr     * @return string
27004a78b87SAndreas Gohr     */
27104a78b87SAndreas Gohr    public function getLabel()
27204a78b87SAndreas Gohr    {
27304a78b87SAndreas Gohr        return ucfirst($this->getServiceID());
27404a78b87SAndreas Gohr    }
27504a78b87SAndreas Gohr
27604a78b87SAndreas Gohr    /**
27704a78b87SAndreas Gohr     * Return the internal name of the Service
27804a78b87SAndreas Gohr     *
27904a78b87SAndreas Gohr     * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in
28004a78b87SAndreas Gohr     * the appropriate lusitantian oauth Service namespace
28104a78b87SAndreas Gohr     *
28204a78b87SAndreas Gohr     * @return string
28304a78b87SAndreas Gohr     */
28404a78b87SAndreas Gohr    public function getServiceID()
28504a78b87SAndreas Gohr    {
28604a78b87SAndreas Gohr        $name = $this->getPluginName();
28704a78b87SAndreas Gohr        if (substr($name, 0, 5) === 'oauth') {
28804a78b87SAndreas Gohr            $name = substr($name, 5);
28904a78b87SAndreas Gohr        }
29004a78b87SAndreas Gohr
29104a78b87SAndreas Gohr        return $name;
29204a78b87SAndreas Gohr    }
29304a78b87SAndreas Gohr
29404a78b87SAndreas Gohr    /**
29504a78b87SAndreas Gohr     * Register a new Service
29604a78b87SAndreas Gohr     *
29704a78b87SAndreas Gohr     * @return string A fully qualified class name to register as new Service for your ServiceID
29804a78b87SAndreas Gohr     */
29904a78b87SAndreas Gohr    public function registerServiceClass()
30004a78b87SAndreas Gohr    {
30104a78b87SAndreas Gohr        return null;
30204a78b87SAndreas Gohr    }
30304a78b87SAndreas Gohr
30404a78b87SAndreas Gohr    /**
30504a78b87SAndreas Gohr     * Return the button color to use
30604a78b87SAndreas Gohr     *
30704a78b87SAndreas Gohr     * @return string
30804a78b87SAndreas Gohr     */
30904a78b87SAndreas Gohr    public function getColor()
31004a78b87SAndreas Gohr    {
31104a78b87SAndreas Gohr        return '#999';
31204a78b87SAndreas Gohr    }
31304a78b87SAndreas Gohr
31404a78b87SAndreas Gohr    /**
31504a78b87SAndreas Gohr     * Return the SVG of the logo for this service
31604a78b87SAndreas Gohr     *
31704a78b87SAndreas Gohr     * Defaults to a logo.svg in the plugin directory
31804a78b87SAndreas Gohr     *
31904a78b87SAndreas Gohr     * @return string
32004a78b87SAndreas Gohr     */
32104a78b87SAndreas Gohr    public function getSvgLogo()
32204a78b87SAndreas Gohr    {
32304a78b87SAndreas Gohr        $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg';
32404a78b87SAndreas Gohr        if (file_exists($logo)) return inlineSVG($logo);
32504a78b87SAndreas Gohr        return '';
32604a78b87SAndreas Gohr    }
32704a78b87SAndreas Gohr
32804a78b87SAndreas Gohr    /**
32904a78b87SAndreas Gohr     * The oauth key
33004a78b87SAndreas Gohr     *
33104a78b87SAndreas Gohr     * @return string
33204a78b87SAndreas Gohr     */
33304a78b87SAndreas Gohr    public function getKey()
33404a78b87SAndreas Gohr    {
33504a78b87SAndreas Gohr        return $this->getConf('key');
33604a78b87SAndreas Gohr    }
33704a78b87SAndreas Gohr
33804a78b87SAndreas Gohr    /**
33904a78b87SAndreas Gohr     * The oauth secret
34004a78b87SAndreas Gohr     *
34104a78b87SAndreas Gohr     * @return string
34204a78b87SAndreas Gohr     */
34304a78b87SAndreas Gohr    public function getSecret()
34404a78b87SAndreas Gohr    {
34504a78b87SAndreas Gohr        return $this->getConf('secret');
34604a78b87SAndreas Gohr    }
34704a78b87SAndreas Gohr
34804a78b87SAndreas Gohr    // endregion
34904a78b87SAndreas Gohr}
350