xref: /plugin/oauth/Adapter.php (revision 28002081b4e69d3951b2d8230c8668e3cbd7f708)
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;
89cbef4d7SAndreas Gohruse OAuth\Common\Storage\Exception\TokenNotFoundException;
9*28002081SAndreas Gohruse OAuth\Common\Storage\Session as SessionStorage;
1004a78b87SAndreas Gohruse OAuth\OAuth1\Service\AbstractService as Abstract1Service;
1104a78b87SAndreas Gohruse OAuth\OAuth1\Token\TokenInterface;
1204a78b87SAndreas Gohruse OAuth\OAuth2\Service\AbstractService as Abstract2Service;
1304a78b87SAndreas Gohruse OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException;
149cbef4d7SAndreas Gohruse OAuth\OAuth2\Service\Exception\MissingRefreshTokenException;
1504a78b87SAndreas Gohruse OAuth\ServiceFactory;
1604a78b87SAndreas Gohr
1704a78b87SAndreas Gohr/**
1804a78b87SAndreas Gohr * Base class to implement a Backend Service for the oAuth Plugin
1904a78b87SAndreas Gohr */
2004a78b87SAndreas Gohrabstract class Adapter extends ActionPlugin
2104a78b87SAndreas Gohr{
2204a78b87SAndreas Gohr    /**
2304a78b87SAndreas Gohr     * @var Abstract2Service|Abstract1Service
2404a78b87SAndreas Gohr     * @see getOAuthService() use this to ensure it's intialized
2504a78b87SAndreas Gohr     */
2604a78b87SAndreas Gohr    protected $oAuth;
2704a78b87SAndreas Gohr
2804a78b87SAndreas Gohr    // region internal methods
2904a78b87SAndreas Gohr
3004a78b87SAndreas Gohr    /**
3104a78b87SAndreas Gohr     * Auto register this plugin with the oAuth authentication plugin
3204a78b87SAndreas Gohr     *
3304a78b87SAndreas Gohr     * @inheritDoc
3404a78b87SAndreas Gohr     */
3504a78b87SAndreas Gohr    public function register(\Doku_Event_Handler $controller)
3604a78b87SAndreas Gohr    {
3704a78b87SAndreas Gohr        $controller->register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister');
3804a78b87SAndreas Gohr    }
3904a78b87SAndreas Gohr
4004a78b87SAndreas Gohr    /**
4104a78b87SAndreas Gohr     * Auto register this plugin with the oAuth authentication plugin
4204a78b87SAndreas Gohr     */
4304a78b87SAndreas Gohr    public function handleRegister(\Doku_Event $event, $param)
4404a78b87SAndreas Gohr    {
4504a78b87SAndreas Gohr        $event->data[$this->getServiceID()] = $this;
4604a78b87SAndreas Gohr    }
4704a78b87SAndreas Gohr
4804a78b87SAndreas Gohr    /**
4904a78b87SAndreas Gohr     * Initialize the oAuth service
5004a78b87SAndreas Gohr     *
51*28002081SAndreas Gohr     * @param string $storageId user based storage key (if available, yet)
5204a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
5304a78b87SAndreas Gohr     */
54*28002081SAndreas Gohr    public function initOAuthService($storageId = '')
5504a78b87SAndreas Gohr    {
5604a78b87SAndreas Gohr        /** @var \helper_plugin_oauth $hlp */
5704a78b87SAndreas Gohr        $hlp = plugin_load('helper', 'oauth');
5804a78b87SAndreas Gohr
5904a78b87SAndreas Gohr        $credentials = new Credentials(
6004a78b87SAndreas Gohr            $this->getKey(),
6104a78b87SAndreas Gohr            $this->getSecret(),
6204a78b87SAndreas Gohr            $hlp->redirectURI()
6304a78b87SAndreas Gohr        );
6404a78b87SAndreas Gohr
6504a78b87SAndreas Gohr        $serviceFactory = new ServiceFactory();
6604a78b87SAndreas Gohr        $serviceFactory->setHttpClient(new HTTPClient());
6704a78b87SAndreas Gohr        $servicename = $this->getServiceID();
6804a78b87SAndreas Gohr        $serviceclass = $this->registerServiceClass();
6904a78b87SAndreas Gohr        if ($serviceclass) {
7004a78b87SAndreas Gohr            $serviceFactory->registerService($servicename, $serviceclass);
7104a78b87SAndreas Gohr        }
7204a78b87SAndreas Gohr
73*28002081SAndreas Gohr        if ($storageId) {
74*28002081SAndreas Gohr            $storage = new Storage($storageId);
75*28002081SAndreas Gohr        } else {
76*28002081SAndreas Gohr            $storage = new SessionStorage();
77*28002081SAndreas Gohr        }
78*28002081SAndreas Gohr
7904a78b87SAndreas Gohr        $this->oAuth = $serviceFactory->createService(
8004a78b87SAndreas Gohr            $servicename,
8104a78b87SAndreas Gohr            $credentials,
82*28002081SAndreas Gohr            $storage,
8304a78b87SAndreas Gohr            $this->getScopes()
8404a78b87SAndreas Gohr        );
8504a78b87SAndreas Gohr
8604a78b87SAndreas Gohr        if ($this->oAuth === null) {
8704a78b87SAndreas Gohr            throw new Exception('Failed to initialize Service ' . $this->getLabel());
8804a78b87SAndreas Gohr        }
8904a78b87SAndreas Gohr    }
9004a78b87SAndreas Gohr
9104a78b87SAndreas Gohr    /**
9204a78b87SAndreas Gohr     * @return Abstract2Service|Abstract1Service
9304a78b87SAndreas Gohr     * @throws Exception
9404a78b87SAndreas Gohr     */
9504a78b87SAndreas Gohr    public function getOAuthService()
9604a78b87SAndreas Gohr    {
9704a78b87SAndreas Gohr        if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized');
9804a78b87SAndreas Gohr        return $this->oAuth;
9904a78b87SAndreas Gohr    }
10004a78b87SAndreas Gohr
10104a78b87SAndreas Gohr    /**
102*28002081SAndreas Gohr     * Once a user has been authenticated, the current token storage needs to be made permanent
103*28002081SAndreas Gohr     *
104*28002081SAndreas Gohr     * @param string $storageId
105*28002081SAndreas Gohr     * @throws Exception
106*28002081SAndreas Gohr     * @throws TokenNotFoundException
107*28002081SAndreas Gohr     */
108*28002081SAndreas Gohr    public function upgradeStorage($storageId)
109*28002081SAndreas Gohr    {
110*28002081SAndreas Gohr        $oauth = $this->getOAuthService();
111*28002081SAndreas Gohr        $service = $oauth->service();
112*28002081SAndreas Gohr
113*28002081SAndreas Gohr        $oldStorage = $oauth->getStorage();
114*28002081SAndreas Gohr        $newStorage = new Storage($storageId);
115*28002081SAndreas Gohr        if ($oldStorage->hasAccessToken($service)) {
116*28002081SAndreas Gohr            $newStorage->storeAccessToken($service, $oldStorage->retrieveAccessToken($service));
117*28002081SAndreas Gohr        }
118*28002081SAndreas Gohr        if ($oldStorage->hasAuthorizationState($service)) {
119*28002081SAndreas Gohr            $newStorage->storeAuthorizationState($service, $oldStorage->retrieveAuthorizationState($service));
120*28002081SAndreas Gohr        }
121*28002081SAndreas Gohr
122*28002081SAndreas Gohr        // fixme invalidate current oauth object? reinitialize it?
123*28002081SAndreas Gohr    }
124*28002081SAndreas Gohr
125*28002081SAndreas Gohr    /**
1269cbef4d7SAndreas Gohr     * Refresh a possibly outdated access token
1279cbef4d7SAndreas Gohr     *
1289cbef4d7SAndreas Gohr     * Does nothing when the current token is still good to use
1299cbef4d7SAndreas Gohr     *
1309cbef4d7SAndreas Gohr     * @return void
1319cbef4d7SAndreas Gohr     * @throws MissingRefreshTokenException
1329cbef4d7SAndreas Gohr     * @throws TokenNotFoundException
1339cbef4d7SAndreas Gohr     * @throws TokenResponseException
1349cbef4d7SAndreas Gohr     * @throws Exception
1359cbef4d7SAndreas Gohr     */
1369cbef4d7SAndreas Gohr    public function refreshOutdatedToken()
1379cbef4d7SAndreas Gohr    {
1389cbef4d7SAndreas Gohr        $oauth = $this->getOAuthService();
1399cbef4d7SAndreas Gohr
1409cbef4d7SAndreas Gohr        if (!$oauth->getStorage()->hasAccessToken($oauth->service())) {
1419cbef4d7SAndreas Gohr            // no token to refresh
1429cbef4d7SAndreas Gohr            return;
1439cbef4d7SAndreas Gohr        }
1449cbef4d7SAndreas Gohr
1459cbef4d7SAndreas Gohr        $token = $oauth->getStorage()->retrieveAccessToken($oauth->service());
1469cbef4d7SAndreas Gohr        if ($token->getEndOfLife() < 0 ||
1479cbef4d7SAndreas Gohr            $token->getEndOfLife() - time() > 3600) {
1489cbef4d7SAndreas Gohr            // token is still good
1499cbef4d7SAndreas Gohr            return;
1509cbef4d7SAndreas Gohr        }
1519cbef4d7SAndreas Gohr
1529cbef4d7SAndreas Gohr        $refreshToken = $token->getRefreshToken();
1539cbef4d7SAndreas Gohr        $token = $oauth->refreshAccessToken($token);
1549cbef4d7SAndreas Gohr
1559cbef4d7SAndreas Gohr        // If the IDP did not provide a new refresh token, store the old one
1569cbef4d7SAndreas Gohr        if (!$token->getRefreshToken()) {
1579cbef4d7SAndreas Gohr            $token->setRefreshToken($refreshToken);
1589cbef4d7SAndreas Gohr            $oauth->getStorage()->storeAccessToken($oauth->service(), $token);
1599cbef4d7SAndreas Gohr        }
1609cbef4d7SAndreas Gohr    }
1619cbef4d7SAndreas Gohr
1629cbef4d7SAndreas Gohr    /**
16304a78b87SAndreas Gohr     * Redirects to the service for requesting access
16404a78b87SAndreas Gohr     *
16504a78b87SAndreas Gohr     * This is the first step of oAuth authentication
16604a78b87SAndreas Gohr     *
16704a78b87SAndreas Gohr     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
16804a78b87SAndreas Gohr     * but might need to be overwritten for specific services
16904a78b87SAndreas Gohr     *
17004a78b87SAndreas Gohr     * @throws TokenResponseException
17104a78b87SAndreas Gohr     * @throws Exception
17204a78b87SAndreas Gohr     */
17304a78b87SAndreas Gohr    public function login()
17404a78b87SAndreas Gohr    {
17504a78b87SAndreas Gohr        $oauth = $this->getOAuthService();
17604a78b87SAndreas Gohr
17704a78b87SAndreas Gohr        // store Farmer animal in oAuth state parameter
17804a78b87SAndreas Gohr        /** @var \helper_plugin_farmer $farmer */
17904a78b87SAndreas Gohr        $farmer = plugin_load('helper', 'farmer');
18004a78b87SAndreas Gohr        $parameters = [];
18104a78b87SAndreas Gohr        if ($farmer && $animal = $farmer->getAnimal()) {
18204a78b87SAndreas Gohr            $parameters['state'] = urlencode(base64_encode(json_encode(
18304a78b87SAndreas Gohr                [
18404a78b87SAndreas Gohr                    'animal' => $animal,
18504a78b87SAndreas Gohr                    'state' => md5(rand()),
18604a78b87SAndreas Gohr                ]
18704a78b87SAndreas Gohr            )));
18804a78b87SAndreas Gohr            $oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']);
18904a78b87SAndreas Gohr        }
19004a78b87SAndreas Gohr
19104a78b87SAndreas Gohr        if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */
19204a78b87SAndreas Gohr            // extra request needed for oauth1 to request a request token
19304a78b87SAndreas Gohr            $token = $oauth->requestRequestToken();
19404a78b87SAndreas Gohr            $parameters['oauth_token'] = $token->getRequestToken();
19504a78b87SAndreas Gohr        }
19604a78b87SAndreas Gohr        $url = $oauth->getAuthorizationUri($parameters);
19704a78b87SAndreas Gohr
19804a78b87SAndreas Gohr        send_redirect($url);
19904a78b87SAndreas Gohr    }
20004a78b87SAndreas Gohr
20104a78b87SAndreas Gohr    /**
20204a78b87SAndreas Gohr     * Request access token
20304a78b87SAndreas Gohr     *
20404a78b87SAndreas Gohr     * This is the second step of oAuth authentication
20504a78b87SAndreas Gohr     *
20604a78b87SAndreas Gohr     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
20704a78b87SAndreas Gohr     * but might need to be overwritten for specific services
20804a78b87SAndreas Gohr     *
20904a78b87SAndreas Gohr     * Thrown exceptions indicate a non-successful login because of some error, appropriate messages
21004a78b87SAndreas Gohr     * should be shown to the user. A return of false with no exceptions indicates that there was no
21104a78b87SAndreas Gohr     * oauth data at all. This can probably be silently ignored.
21204a78b87SAndreas Gohr     *
21304a78b87SAndreas Gohr     * @return bool true if authentication was successful
21404a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
21504a78b87SAndreas Gohr     * @throws InvalidAuthorizationStateException
21604a78b87SAndreas Gohr     */
21704a78b87SAndreas Gohr    public function checkToken()
21804a78b87SAndreas Gohr    {
21904a78b87SAndreas Gohr        global $INPUT;
22004a78b87SAndreas Gohr
22104a78b87SAndreas Gohr        $oauth = $this->getOAuthService();
22204a78b87SAndreas Gohr
22304a78b87SAndreas Gohr        if (is_a($oauth, Abstract2Service::class)) {
22404a78b87SAndreas Gohr            /** @var Abstract2Service $oauth */
22504a78b87SAndreas Gohr            if (!$INPUT->get->has('code')) return false;
22604a78b87SAndreas Gohr            $state = $INPUT->get->str('state', null);
227eae50416SAndreas Gohr            $accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state);
22804a78b87SAndreas Gohr        } else {
22904a78b87SAndreas Gohr            /** @var Abstract1Service $oauth */
23004a78b87SAndreas Gohr            if (!$INPUT->get->has('oauth_token')) return false;
23104a78b87SAndreas Gohr            /** @var TokenInterface $token */
23204a78b87SAndreas Gohr            $token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID());
233eae50416SAndreas Gohr            $accessToken = $oauth->requestAccessToken(
23404a78b87SAndreas Gohr                $INPUT->get->str('oauth_token'),
23504a78b87SAndreas Gohr                $INPUT->get->str('oauth_verifier'),
23604a78b87SAndreas Gohr                $token->getRequestTokenSecret()
23704a78b87SAndreas Gohr            );
23804a78b87SAndreas Gohr        }
239eae50416SAndreas Gohr
240eae50416SAndreas Gohr        if (
241eae50416SAndreas Gohr            $accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES &&
242eae50416SAndreas Gohr            !$accessToken->getRefreshToken()) {
243eae50416SAndreas Gohr            msg('Service did not provide a Refresh Token. You will be logged out when the session expires.');
244eae50416SAndreas Gohr        }
245eae50416SAndreas Gohr
24604a78b87SAndreas Gohr        return true;
24704a78b87SAndreas Gohr    }
24804a78b87SAndreas Gohr
24904a78b87SAndreas Gohr    /**
25004a78b87SAndreas Gohr     * Return the Service Login Button
25104a78b87SAndreas Gohr     *
25204a78b87SAndreas Gohr     * @return string
25304a78b87SAndreas Gohr     */
25404a78b87SAndreas Gohr    public function loginButton()
25504a78b87SAndreas Gohr    {
25604a78b87SAndreas Gohr        global $ID;
25704a78b87SAndreas Gohr
25804a78b87SAndreas Gohr        $attr = buildAttributes([
25904a78b87SAndreas Gohr            'href' => wl($ID, array('oauthlogin' => $this->getServiceID()), false, '&'),
26004a78b87SAndreas Gohr            'class' => 'plugin_oauth_' . $this->getServiceID(),
26104a78b87SAndreas Gohr            'style' => 'background-color: ' . $this->getColor(),
26204a78b87SAndreas Gohr        ]);
26304a78b87SAndreas Gohr
26404a78b87SAndreas Gohr        return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> ';
26504a78b87SAndreas Gohr    }
26604a78b87SAndreas Gohr    // endregion
26704a78b87SAndreas Gohr
26804a78b87SAndreas Gohr    // region overridable methods
26904a78b87SAndreas Gohr
27004a78b87SAndreas Gohr    /**
27104a78b87SAndreas Gohr     * Retrieve the user's data via API
27204a78b87SAndreas Gohr     *
27304a78b87SAndreas Gohr     * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps'
27404a78b87SAndreas Gohr     *
27504a78b87SAndreas Gohr     * Use the request() method of the oauth object to talk to the API
27604a78b87SAndreas Gohr     *
27704a78b87SAndreas Gohr     * @return array
27804a78b87SAndreas Gohr     * @throws Exception
27904a78b87SAndreas Gohr     * @see getOAuthService()
28004a78b87SAndreas Gohr     */
28104a78b87SAndreas Gohr    abstract public function getUser();
28204a78b87SAndreas Gohr
28304a78b87SAndreas Gohr    /**
28404a78b87SAndreas Gohr     * Return the scopes to request
28504a78b87SAndreas Gohr     *
28604a78b87SAndreas Gohr     * This should return the minimal scopes needed for accessing the user's data
28704a78b87SAndreas Gohr     *
28804a78b87SAndreas Gohr     * @return string[]
28904a78b87SAndreas Gohr     */
29004a78b87SAndreas Gohr    public function getScopes()
29104a78b87SAndreas Gohr    {
29204a78b87SAndreas Gohr        return [];
29304a78b87SAndreas Gohr    }
29404a78b87SAndreas Gohr
29504a78b87SAndreas Gohr    /**
29604a78b87SAndreas Gohr     * Return the user friendly name of the service
29704a78b87SAndreas Gohr     *
29804a78b87SAndreas Gohr     * Defaults to ServiceID. You may want to override this.
29904a78b87SAndreas Gohr     *
30004a78b87SAndreas Gohr     * @return string
30104a78b87SAndreas Gohr     */
30204a78b87SAndreas Gohr    public function getLabel()
30304a78b87SAndreas Gohr    {
30404a78b87SAndreas Gohr        return ucfirst($this->getServiceID());
30504a78b87SAndreas Gohr    }
30604a78b87SAndreas Gohr
30704a78b87SAndreas Gohr    /**
30804a78b87SAndreas Gohr     * Return the internal name of the Service
30904a78b87SAndreas Gohr     *
31004a78b87SAndreas Gohr     * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in
31104a78b87SAndreas Gohr     * the appropriate lusitantian oauth Service namespace
31204a78b87SAndreas Gohr     *
31304a78b87SAndreas Gohr     * @return string
31404a78b87SAndreas Gohr     */
31504a78b87SAndreas Gohr    public function getServiceID()
31604a78b87SAndreas Gohr    {
31704a78b87SAndreas Gohr        $name = $this->getPluginName();
31804a78b87SAndreas Gohr        if (substr($name, 0, 5) === 'oauth') {
31904a78b87SAndreas Gohr            $name = substr($name, 5);
32004a78b87SAndreas Gohr        }
32104a78b87SAndreas Gohr
32204a78b87SAndreas Gohr        return $name;
32304a78b87SAndreas Gohr    }
32404a78b87SAndreas Gohr
32504a78b87SAndreas Gohr    /**
32604a78b87SAndreas Gohr     * Register a new Service
32704a78b87SAndreas Gohr     *
32804a78b87SAndreas Gohr     * @return string A fully qualified class name to register as new Service for your ServiceID
32904a78b87SAndreas Gohr     */
33004a78b87SAndreas Gohr    public function registerServiceClass()
33104a78b87SAndreas Gohr    {
33204a78b87SAndreas Gohr        return null;
33304a78b87SAndreas Gohr    }
33404a78b87SAndreas Gohr
33504a78b87SAndreas Gohr    /**
33604a78b87SAndreas Gohr     * Return the button color to use
33704a78b87SAndreas Gohr     *
33804a78b87SAndreas Gohr     * @return string
33904a78b87SAndreas Gohr     */
34004a78b87SAndreas Gohr    public function getColor()
34104a78b87SAndreas Gohr    {
34204a78b87SAndreas Gohr        return '#999';
34304a78b87SAndreas Gohr    }
34404a78b87SAndreas Gohr
34504a78b87SAndreas Gohr    /**
34604a78b87SAndreas Gohr     * Return the SVG of the logo for this service
34704a78b87SAndreas Gohr     *
34804a78b87SAndreas Gohr     * Defaults to a logo.svg in the plugin directory
34904a78b87SAndreas Gohr     *
35004a78b87SAndreas Gohr     * @return string
35104a78b87SAndreas Gohr     */
35204a78b87SAndreas Gohr    public function getSvgLogo()
35304a78b87SAndreas Gohr    {
35404a78b87SAndreas Gohr        $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg';
35504a78b87SAndreas Gohr        if (file_exists($logo)) return inlineSVG($logo);
35604a78b87SAndreas Gohr        return '';
35704a78b87SAndreas Gohr    }
35804a78b87SAndreas Gohr
35904a78b87SAndreas Gohr    /**
36004a78b87SAndreas Gohr     * The oauth key
36104a78b87SAndreas Gohr     *
36204a78b87SAndreas Gohr     * @return string
36304a78b87SAndreas Gohr     */
36404a78b87SAndreas Gohr    public function getKey()
36504a78b87SAndreas Gohr    {
36604a78b87SAndreas Gohr        return $this->getConf('key');
36704a78b87SAndreas Gohr    }
36804a78b87SAndreas Gohr
36904a78b87SAndreas Gohr    /**
37004a78b87SAndreas Gohr     * The oauth secret
37104a78b87SAndreas Gohr     *
37204a78b87SAndreas Gohr     * @return string
37304a78b87SAndreas Gohr     */
37404a78b87SAndreas Gohr    public function getSecret()
37504a78b87SAndreas Gohr    {
37604a78b87SAndreas Gohr        return $this->getConf('secret');
37704a78b87SAndreas Gohr    }
37804a78b87SAndreas Gohr
37904a78b87SAndreas Gohr    // endregion
38004a78b87SAndreas Gohr}
381