xref: /plugin/oauth/Adapter.php (revision eae50416f9f9034fe944627716b2567b617dfac2)
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;
804a78b87SAndreas Gohruse OAuth\OAuth1\Service\AbstractService as Abstract1Service;
904a78b87SAndreas Gohruse OAuth\OAuth1\Token\TokenInterface;
1004a78b87SAndreas Gohruse OAuth\OAuth2\Service\AbstractService as Abstract2Service;
1104a78b87SAndreas Gohruse OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException;
1204a78b87SAndreas Gohruse OAuth\ServiceFactory;
1304a78b87SAndreas Gohr
1404a78b87SAndreas Gohr/**
1504a78b87SAndreas Gohr * Base class to implement a Backend Service for the oAuth Plugin
1604a78b87SAndreas Gohr */
1704a78b87SAndreas Gohrabstract class Adapter extends ActionPlugin
1804a78b87SAndreas Gohr{
1904a78b87SAndreas Gohr    /**
2004a78b87SAndreas Gohr     * @var Abstract2Service|Abstract1Service
2104a78b87SAndreas Gohr     * @see getOAuthService() use this to ensure it's intialized
2204a78b87SAndreas Gohr     */
2304a78b87SAndreas Gohr    protected $oAuth;
2404a78b87SAndreas Gohr
2504a78b87SAndreas Gohr    // region internal methods
2604a78b87SAndreas Gohr
2704a78b87SAndreas Gohr    /**
2804a78b87SAndreas Gohr     * Auto register this plugin with the oAuth authentication plugin
2904a78b87SAndreas Gohr     *
3004a78b87SAndreas Gohr     * @inheritDoc
3104a78b87SAndreas Gohr     */
3204a78b87SAndreas Gohr    public function register(\Doku_Event_Handler $controller)
3304a78b87SAndreas Gohr    {
3404a78b87SAndreas Gohr        $controller->register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister');
3504a78b87SAndreas Gohr    }
3604a78b87SAndreas Gohr
3704a78b87SAndreas Gohr    /**
3804a78b87SAndreas Gohr     * Auto register this plugin with the oAuth authentication plugin
3904a78b87SAndreas Gohr     */
4004a78b87SAndreas Gohr    public function handleRegister(\Doku_Event $event, $param)
4104a78b87SAndreas Gohr    {
4204a78b87SAndreas Gohr        $event->data[$this->getServiceID()] = $this;
4304a78b87SAndreas Gohr    }
4404a78b87SAndreas Gohr
4504a78b87SAndreas Gohr    /**
4604a78b87SAndreas Gohr     * Initialize the oAuth service
4704a78b87SAndreas Gohr     *
4804a78b87SAndreas Gohr     * @param string $guid UIID for the user to authenticate
4904a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
5004a78b87SAndreas Gohr     */
5104a78b87SAndreas Gohr    public function initOAuthService($guid)
5204a78b87SAndreas Gohr    {
5304a78b87SAndreas Gohr        /** @var \helper_plugin_oauth $hlp */
5404a78b87SAndreas Gohr        $hlp = plugin_load('helper', 'oauth');
5504a78b87SAndreas Gohr
5604a78b87SAndreas Gohr        $credentials = new Credentials(
5704a78b87SAndreas Gohr            $this->getKey(),
5804a78b87SAndreas Gohr            $this->getSecret(),
5904a78b87SAndreas Gohr            $hlp->redirectURI()
6004a78b87SAndreas Gohr        );
6104a78b87SAndreas Gohr
6204a78b87SAndreas Gohr        $serviceFactory = new ServiceFactory();
6304a78b87SAndreas Gohr        $serviceFactory->setHttpClient(new HTTPClient());
6404a78b87SAndreas Gohr        $servicename = $this->getServiceID();
6504a78b87SAndreas Gohr        $serviceclass = $this->registerServiceClass();
6604a78b87SAndreas Gohr        if ($serviceclass) {
6704a78b87SAndreas Gohr            $serviceFactory->registerService($servicename, $serviceclass);
6804a78b87SAndreas Gohr        }
6904a78b87SAndreas Gohr
7004a78b87SAndreas Gohr        $this->oAuth = $serviceFactory->createService(
7104a78b87SAndreas Gohr            $servicename,
7204a78b87SAndreas Gohr            $credentials,
7304a78b87SAndreas Gohr            new Storage($guid),
7404a78b87SAndreas Gohr            $this->getScopes()
7504a78b87SAndreas Gohr        );
7604a78b87SAndreas Gohr
7704a78b87SAndreas Gohr        if ($this->oAuth === null) {
7804a78b87SAndreas Gohr            throw new Exception('Failed to initialize Service ' . $this->getLabel());
7904a78b87SAndreas Gohr        }
8004a78b87SAndreas Gohr    }
8104a78b87SAndreas Gohr
8204a78b87SAndreas Gohr    /**
8304a78b87SAndreas Gohr     * @return Abstract2Service|Abstract1Service
8404a78b87SAndreas Gohr     * @throws Exception
8504a78b87SAndreas Gohr     */
8604a78b87SAndreas Gohr    public function getOAuthService()
8704a78b87SAndreas Gohr    {
8804a78b87SAndreas Gohr        if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized');
8904a78b87SAndreas Gohr        return $this->oAuth;
9004a78b87SAndreas Gohr    }
9104a78b87SAndreas Gohr
9204a78b87SAndreas Gohr    /**
9304a78b87SAndreas Gohr     * Redirects to the service for requesting access
9404a78b87SAndreas Gohr     *
9504a78b87SAndreas Gohr     * This is the first step of oAuth authentication
9604a78b87SAndreas Gohr     *
9704a78b87SAndreas Gohr     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
9804a78b87SAndreas Gohr     * but might need to be overwritten for specific services
9904a78b87SAndreas Gohr     *
10004a78b87SAndreas Gohr     * @throws TokenResponseException
10104a78b87SAndreas Gohr     * @throws Exception
10204a78b87SAndreas Gohr     */
10304a78b87SAndreas Gohr    public function login()
10404a78b87SAndreas Gohr    {
10504a78b87SAndreas Gohr        $oauth = $this->getOAuthService();
10604a78b87SAndreas Gohr
10704a78b87SAndreas Gohr        // store Farmer animal in oAuth state parameter
10804a78b87SAndreas Gohr        /** @var \helper_plugin_farmer $farmer */
10904a78b87SAndreas Gohr        $farmer = plugin_load('helper', 'farmer');
11004a78b87SAndreas Gohr        $parameters = [];
11104a78b87SAndreas Gohr        if ($farmer && $animal = $farmer->getAnimal()) {
11204a78b87SAndreas Gohr            $parameters['state'] = urlencode(base64_encode(json_encode(
11304a78b87SAndreas Gohr                [
11404a78b87SAndreas Gohr                    'animal' => $animal,
11504a78b87SAndreas Gohr                    'state' => md5(rand()),
11604a78b87SAndreas Gohr                ]
11704a78b87SAndreas Gohr            )));
11804a78b87SAndreas Gohr            $oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']);
11904a78b87SAndreas Gohr        }
12004a78b87SAndreas Gohr
12104a78b87SAndreas Gohr        if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */
12204a78b87SAndreas Gohr            // extra request needed for oauth1 to request a request token
12304a78b87SAndreas Gohr            $token = $oauth->requestRequestToken();
12404a78b87SAndreas Gohr            $parameters['oauth_token'] = $token->getRequestToken();
12504a78b87SAndreas Gohr        }
12604a78b87SAndreas Gohr        $url = $oauth->getAuthorizationUri($parameters);
12704a78b87SAndreas Gohr
12804a78b87SAndreas Gohr        send_redirect($url);
12904a78b87SAndreas Gohr    }
13004a78b87SAndreas Gohr
13104a78b87SAndreas Gohr    /**
13204a78b87SAndreas Gohr     * Request access token
13304a78b87SAndreas Gohr     *
13404a78b87SAndreas Gohr     * This is the second 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     * Thrown exceptions indicate a non-successful login because of some error, appropriate messages
14004a78b87SAndreas Gohr     * should be shown to the user. A return of false with no exceptions indicates that there was no
14104a78b87SAndreas Gohr     * oauth data at all. This can probably be silently ignored.
14204a78b87SAndreas Gohr     *
14304a78b87SAndreas Gohr     * @return bool true if authentication was successful
14404a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
14504a78b87SAndreas Gohr     * @throws InvalidAuthorizationStateException
14604a78b87SAndreas Gohr     */
14704a78b87SAndreas Gohr    public function checkToken()
14804a78b87SAndreas Gohr    {
14904a78b87SAndreas Gohr        global $INPUT;
15004a78b87SAndreas Gohr
15104a78b87SAndreas Gohr        $oauth = $this->getOAuthService();
15204a78b87SAndreas Gohr
15304a78b87SAndreas Gohr        if (is_a($oauth, Abstract2Service::class)) {
15404a78b87SAndreas Gohr            /** @var Abstract2Service $oauth */
15504a78b87SAndreas Gohr            if (!$INPUT->get->has('code')) return false;
15604a78b87SAndreas Gohr            $state = $INPUT->get->str('state', null);
157*eae50416SAndreas Gohr            $accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state);
15804a78b87SAndreas Gohr        } else {
15904a78b87SAndreas Gohr            /** @var Abstract1Service $oauth */
16004a78b87SAndreas Gohr            if (!$INPUT->get->has('oauth_token')) return false;
16104a78b87SAndreas Gohr            /** @var TokenInterface $token */
16204a78b87SAndreas Gohr            $token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID());
163*eae50416SAndreas Gohr            $accessToken = $oauth->requestAccessToken(
16404a78b87SAndreas Gohr                $INPUT->get->str('oauth_token'),
16504a78b87SAndreas Gohr                $INPUT->get->str('oauth_verifier'),
16604a78b87SAndreas Gohr                $token->getRequestTokenSecret()
16704a78b87SAndreas Gohr            );
16804a78b87SAndreas Gohr        }
169*eae50416SAndreas Gohr
170*eae50416SAndreas Gohr        if (
171*eae50416SAndreas Gohr            $accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES &&
172*eae50416SAndreas Gohr            !$accessToken->getRefreshToken()) {
173*eae50416SAndreas Gohr            msg('Service did not provide a Refresh Token. You will be logged out when the session expires.');
174*eae50416SAndreas Gohr        }
175*eae50416SAndreas Gohr
17604a78b87SAndreas Gohr        return true;
17704a78b87SAndreas Gohr    }
17804a78b87SAndreas Gohr
17904a78b87SAndreas Gohr    /**
18004a78b87SAndreas Gohr     * Return the Service Login Button
18104a78b87SAndreas Gohr     *
18204a78b87SAndreas Gohr     * @return string
18304a78b87SAndreas Gohr     */
18404a78b87SAndreas Gohr    public function loginButton()
18504a78b87SAndreas Gohr    {
18604a78b87SAndreas Gohr        global $ID;
18704a78b87SAndreas Gohr
18804a78b87SAndreas Gohr        $attr = buildAttributes([
18904a78b87SAndreas Gohr            'href' => wl($ID, array('oauthlogin' => $this->getServiceID()), false, '&'),
19004a78b87SAndreas Gohr            'class' => 'plugin_oauth_' . $this->getServiceID(),
19104a78b87SAndreas Gohr            'style' => 'background-color: ' . $this->getColor(),
19204a78b87SAndreas Gohr        ]);
19304a78b87SAndreas Gohr
19404a78b87SAndreas Gohr        return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> ';
19504a78b87SAndreas Gohr    }
19604a78b87SAndreas Gohr    // endregion
19704a78b87SAndreas Gohr
19804a78b87SAndreas Gohr    // region overridable methods
19904a78b87SAndreas Gohr
20004a78b87SAndreas Gohr    /**
20104a78b87SAndreas Gohr     * Retrieve the user's data via API
20204a78b87SAndreas Gohr     *
20304a78b87SAndreas Gohr     * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps'
20404a78b87SAndreas Gohr     *
20504a78b87SAndreas Gohr     * Use the request() method of the oauth object to talk to the API
20604a78b87SAndreas Gohr     *
20704a78b87SAndreas Gohr     * @return array
20804a78b87SAndreas Gohr     * @throws Exception
20904a78b87SAndreas Gohr     * @see getOAuthService()
21004a78b87SAndreas Gohr     */
21104a78b87SAndreas Gohr    abstract public function getUser();
21204a78b87SAndreas Gohr
21304a78b87SAndreas Gohr    /**
21404a78b87SAndreas Gohr     * Return the scopes to request
21504a78b87SAndreas Gohr     *
21604a78b87SAndreas Gohr     * This should return the minimal scopes needed for accessing the user's data
21704a78b87SAndreas Gohr     *
21804a78b87SAndreas Gohr     * @return string[]
21904a78b87SAndreas Gohr     */
22004a78b87SAndreas Gohr    public function getScopes()
22104a78b87SAndreas Gohr    {
22204a78b87SAndreas Gohr        return [];
22304a78b87SAndreas Gohr    }
22404a78b87SAndreas Gohr
22504a78b87SAndreas Gohr    /**
22604a78b87SAndreas Gohr     * Return the user friendly name of the service
22704a78b87SAndreas Gohr     *
22804a78b87SAndreas Gohr     * Defaults to ServiceID. You may want to override this.
22904a78b87SAndreas Gohr     *
23004a78b87SAndreas Gohr     * @return string
23104a78b87SAndreas Gohr     */
23204a78b87SAndreas Gohr    public function getLabel()
23304a78b87SAndreas Gohr    {
23404a78b87SAndreas Gohr        return ucfirst($this->getServiceID());
23504a78b87SAndreas Gohr    }
23604a78b87SAndreas Gohr
23704a78b87SAndreas Gohr    /**
23804a78b87SAndreas Gohr     * Return the internal name of the Service
23904a78b87SAndreas Gohr     *
24004a78b87SAndreas Gohr     * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in
24104a78b87SAndreas Gohr     * the appropriate lusitantian oauth Service namespace
24204a78b87SAndreas Gohr     *
24304a78b87SAndreas Gohr     * @return string
24404a78b87SAndreas Gohr     */
24504a78b87SAndreas Gohr    public function getServiceID()
24604a78b87SAndreas Gohr    {
24704a78b87SAndreas Gohr        $name = $this->getPluginName();
24804a78b87SAndreas Gohr        if (substr($name, 0, 5) === 'oauth') {
24904a78b87SAndreas Gohr            $name = substr($name, 5);
25004a78b87SAndreas Gohr        }
25104a78b87SAndreas Gohr
25204a78b87SAndreas Gohr        return $name;
25304a78b87SAndreas Gohr    }
25404a78b87SAndreas Gohr
25504a78b87SAndreas Gohr    /**
25604a78b87SAndreas Gohr     * Register a new Service
25704a78b87SAndreas Gohr     *
25804a78b87SAndreas Gohr     * @return string A fully qualified class name to register as new Service for your ServiceID
25904a78b87SAndreas Gohr     */
26004a78b87SAndreas Gohr    public function registerServiceClass()
26104a78b87SAndreas Gohr    {
26204a78b87SAndreas Gohr        return null;
26304a78b87SAndreas Gohr    }
26404a78b87SAndreas Gohr
26504a78b87SAndreas Gohr    /**
26604a78b87SAndreas Gohr     * Return the button color to use
26704a78b87SAndreas Gohr     *
26804a78b87SAndreas Gohr     * @return string
26904a78b87SAndreas Gohr     */
27004a78b87SAndreas Gohr    public function getColor()
27104a78b87SAndreas Gohr    {
27204a78b87SAndreas Gohr        return '#999';
27304a78b87SAndreas Gohr    }
27404a78b87SAndreas Gohr
27504a78b87SAndreas Gohr    /**
27604a78b87SAndreas Gohr     * Return the SVG of the logo for this service
27704a78b87SAndreas Gohr     *
27804a78b87SAndreas Gohr     * Defaults to a logo.svg in the plugin directory
27904a78b87SAndreas Gohr     *
28004a78b87SAndreas Gohr     * @return string
28104a78b87SAndreas Gohr     */
28204a78b87SAndreas Gohr    public function getSvgLogo()
28304a78b87SAndreas Gohr    {
28404a78b87SAndreas Gohr        $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg';
28504a78b87SAndreas Gohr        if (file_exists($logo)) return inlineSVG($logo);
28604a78b87SAndreas Gohr        return '';
28704a78b87SAndreas Gohr    }
28804a78b87SAndreas Gohr
28904a78b87SAndreas Gohr    /**
29004a78b87SAndreas Gohr     * The oauth key
29104a78b87SAndreas Gohr     *
29204a78b87SAndreas Gohr     * @return string
29304a78b87SAndreas Gohr     */
29404a78b87SAndreas Gohr    public function getKey()
29504a78b87SAndreas Gohr    {
29604a78b87SAndreas Gohr        return $this->getConf('key');
29704a78b87SAndreas Gohr    }
29804a78b87SAndreas Gohr
29904a78b87SAndreas Gohr    /**
30004a78b87SAndreas Gohr     * The oauth secret
30104a78b87SAndreas Gohr     *
30204a78b87SAndreas Gohr     * @return string
30304a78b87SAndreas Gohr     */
30404a78b87SAndreas Gohr    public function getSecret()
30504a78b87SAndreas Gohr    {
30604a78b87SAndreas Gohr        return $this->getConf('secret');
30704a78b87SAndreas Gohr    }
30804a78b87SAndreas Gohr
30904a78b87SAndreas Gohr    // endregion
31004a78b87SAndreas Gohr}
311