xref: /plugin/oauth/Adapter.php (revision 290e9b1f10bf4135eb82799dc58da0055064d995)
104a78b87SAndreas Gohr<?php
204a78b87SAndreas Gohr
304a78b87SAndreas Gohrnamespace dokuwiki\plugin\oauth;
404a78b87SAndreas Gohr
5*290e9b1fSAndreas Gohruse dokuwiki\Extension\EventHandler;
6*290e9b1fSAndreas Gohruse dokuwiki\Extension\Event;
704a78b87SAndreas Gohruse dokuwiki\Extension\ActionPlugin;
804a78b87SAndreas Gohruse OAuth\Common\Consumer\Credentials;
904a78b87SAndreas Gohruse OAuth\Common\Http\Exception\TokenResponseException;
109cbef4d7SAndreas Gohruse OAuth\Common\Storage\Exception\TokenNotFoundException;
1128002081SAndreas Gohruse OAuth\Common\Storage\Session as SessionStorage;
1204a78b87SAndreas Gohruse OAuth\OAuth1\Service\AbstractService as Abstract1Service;
1304a78b87SAndreas Gohruse OAuth\OAuth1\Token\TokenInterface;
1404a78b87SAndreas Gohruse OAuth\OAuth2\Service\AbstractService as Abstract2Service;
1504a78b87SAndreas Gohruse OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException;
169cbef4d7SAndreas Gohruse OAuth\OAuth2\Service\Exception\MissingRefreshTokenException;
1704a78b87SAndreas Gohruse OAuth\ServiceFactory;
1804a78b87SAndreas Gohr
1904a78b87SAndreas Gohr/**
2004a78b87SAndreas Gohr * Base class to implement a Backend Service for the oAuth Plugin
2104a78b87SAndreas Gohr */
2204a78b87SAndreas Gohrabstract class Adapter extends ActionPlugin
2304a78b87SAndreas Gohr{
2404a78b87SAndreas Gohr    /**
2504a78b87SAndreas Gohr     * @var Abstract2Service|Abstract1Service
2604a78b87SAndreas Gohr     * @see getOAuthService() use this to ensure it's intialized
2704a78b87SAndreas Gohr     */
2804a78b87SAndreas Gohr    protected $oAuth;
2904a78b87SAndreas Gohr
3004a78b87SAndreas Gohr    // region internal methods
3104a78b87SAndreas Gohr
3204a78b87SAndreas Gohr    /**
3304a78b87SAndreas Gohr     * Auto register this plugin with the oAuth authentication plugin
3404a78b87SAndreas Gohr     *
3504a78b87SAndreas Gohr     * @inheritDoc
3604a78b87SAndreas Gohr     */
37*290e9b1fSAndreas Gohr    public function register(EventHandler $controller)
3804a78b87SAndreas Gohr    {
3904a78b87SAndreas Gohr        $controller->register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister');
4004a78b87SAndreas Gohr    }
4104a78b87SAndreas Gohr
4204a78b87SAndreas Gohr    /**
4304a78b87SAndreas Gohr     * Auto register this plugin with the oAuth authentication plugin
4404a78b87SAndreas Gohr     */
45*290e9b1fSAndreas Gohr    public function handleRegister(Event $event, $param)
4604a78b87SAndreas Gohr    {
4704a78b87SAndreas Gohr        $event->data[$this->getServiceID()] = $this;
4804a78b87SAndreas Gohr    }
4904a78b87SAndreas Gohr
5004a78b87SAndreas Gohr    /**
5104a78b87SAndreas Gohr     * Initialize the oAuth service
5204a78b87SAndreas Gohr     *
5328002081SAndreas Gohr     * @param string $storageId user based storage key (if available, yet)
5404a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
5504a78b87SAndreas Gohr     */
5628002081SAndreas Gohr    public function initOAuthService($storageId = '')
5704a78b87SAndreas Gohr    {
5804a78b87SAndreas Gohr        /** @var \helper_plugin_oauth $hlp */
5904a78b87SAndreas Gohr        $hlp = plugin_load('helper', 'oauth');
6004a78b87SAndreas Gohr
6104a78b87SAndreas Gohr        $credentials = new Credentials(
6204a78b87SAndreas Gohr            $this->getKey(),
6304a78b87SAndreas Gohr            $this->getSecret(),
6404a78b87SAndreas Gohr            $hlp->redirectURI()
6504a78b87SAndreas Gohr        );
6604a78b87SAndreas Gohr
6704a78b87SAndreas Gohr        $serviceFactory = new ServiceFactory();
6804a78b87SAndreas Gohr        $serviceFactory->setHttpClient(new HTTPClient());
69*290e9b1fSAndreas Gohr
7004a78b87SAndreas Gohr        $servicename = $this->getServiceID();
7104a78b87SAndreas Gohr        $serviceclass = $this->registerServiceClass();
7204a78b87SAndreas Gohr        if ($serviceclass) {
7304a78b87SAndreas Gohr            $serviceFactory->registerService($servicename, $serviceclass);
7404a78b87SAndreas Gohr        }
7504a78b87SAndreas Gohr
7628002081SAndreas Gohr        if ($storageId) {
7728002081SAndreas Gohr            $storage = new Storage($storageId);
7828002081SAndreas Gohr        } else {
7928002081SAndreas Gohr            $storage = new SessionStorage();
8028002081SAndreas Gohr        }
8128002081SAndreas Gohr
8204a78b87SAndreas Gohr        $this->oAuth = $serviceFactory->createService(
8304a78b87SAndreas Gohr            $servicename,
8404a78b87SAndreas Gohr            $credentials,
8528002081SAndreas Gohr            $storage,
8604a78b87SAndreas Gohr            $this->getScopes()
8704a78b87SAndreas Gohr        );
8804a78b87SAndreas Gohr
8904a78b87SAndreas Gohr        if ($this->oAuth === null) {
9004a78b87SAndreas Gohr            throw new Exception('Failed to initialize Service ' . $this->getLabel());
9104a78b87SAndreas Gohr        }
9204a78b87SAndreas Gohr    }
9304a78b87SAndreas Gohr
9404a78b87SAndreas Gohr    /**
9504a78b87SAndreas Gohr     * @return Abstract2Service|Abstract1Service
9604a78b87SAndreas Gohr     * @throws Exception
9704a78b87SAndreas Gohr     */
9804a78b87SAndreas Gohr    public function getOAuthService()
9904a78b87SAndreas Gohr    {
10004a78b87SAndreas Gohr        if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized');
10104a78b87SAndreas Gohr        return $this->oAuth;
10204a78b87SAndreas Gohr    }
10304a78b87SAndreas Gohr
10404a78b87SAndreas Gohr    /**
10528002081SAndreas Gohr     * Once a user has been authenticated, the current token storage needs to be made permanent
10628002081SAndreas Gohr     *
10728002081SAndreas Gohr     * @param string $storageId
10828002081SAndreas Gohr     * @throws Exception
10928002081SAndreas Gohr     * @throws TokenNotFoundException
11028002081SAndreas Gohr     */
11128002081SAndreas Gohr    public function upgradeStorage($storageId)
11228002081SAndreas Gohr    {
11328002081SAndreas Gohr        $oauth = $this->getOAuthService();
11428002081SAndreas Gohr        $service = $oauth->service();
11528002081SAndreas Gohr
11628002081SAndreas Gohr        $oldStorage = $oauth->getStorage();
11728002081SAndreas Gohr        $newStorage = new Storage($storageId);
11828002081SAndreas Gohr        if ($oldStorage->hasAccessToken($service)) {
11928002081SAndreas Gohr            $newStorage->storeAccessToken($service, $oldStorage->retrieveAccessToken($service));
12028002081SAndreas Gohr        }
12128002081SAndreas Gohr        if ($oldStorage->hasAuthorizationState($service)) {
12228002081SAndreas Gohr            $newStorage->storeAuthorizationState($service, $oldStorage->retrieveAuthorizationState($service));
12328002081SAndreas Gohr        }
12428002081SAndreas Gohr
12528002081SAndreas Gohr        // fixme invalidate current oauth object? reinitialize it?
12628002081SAndreas Gohr    }
12728002081SAndreas Gohr
12828002081SAndreas Gohr    /**
1299cbef4d7SAndreas Gohr     * Refresh a possibly outdated access token
1309cbef4d7SAndreas Gohr     *
1319cbef4d7SAndreas Gohr     * Does nothing when the current token is still good to use
1329cbef4d7SAndreas Gohr     *
1339cbef4d7SAndreas Gohr     * @return void
1349cbef4d7SAndreas Gohr     * @throws MissingRefreshTokenException
1359cbef4d7SAndreas Gohr     * @throws TokenNotFoundException
1369cbef4d7SAndreas Gohr     * @throws TokenResponseException
1379cbef4d7SAndreas Gohr     * @throws Exception
1389cbef4d7SAndreas Gohr     */
1399cbef4d7SAndreas Gohr    public function refreshOutdatedToken()
1409cbef4d7SAndreas Gohr    {
1419cbef4d7SAndreas Gohr        $oauth = $this->getOAuthService();
1429cbef4d7SAndreas Gohr
1439cbef4d7SAndreas Gohr        if (!$oauth->getStorage()->hasAccessToken($oauth->service())) {
1449cbef4d7SAndreas Gohr            // no token to refresh
1459cbef4d7SAndreas Gohr            return;
1469cbef4d7SAndreas Gohr        }
1479cbef4d7SAndreas Gohr
1489cbef4d7SAndreas Gohr        $token = $oauth->getStorage()->retrieveAccessToken($oauth->service());
149*290e9b1fSAndreas Gohr        if (
150*290e9b1fSAndreas Gohr            $token->getEndOfLife() < 0 ||
151*290e9b1fSAndreas Gohr            $token->getEndOfLife() - time() > 3600
152*290e9b1fSAndreas Gohr        ) {
1539cbef4d7SAndreas Gohr            // token is still good
1549cbef4d7SAndreas Gohr            return;
1559cbef4d7SAndreas Gohr        }
1569cbef4d7SAndreas Gohr
1579cbef4d7SAndreas Gohr        $refreshToken = $token->getRefreshToken();
1589cbef4d7SAndreas Gohr        $token = $oauth->refreshAccessToken($token);
1599cbef4d7SAndreas Gohr
1609cbef4d7SAndreas Gohr        // If the IDP did not provide a new refresh token, store the old one
1619cbef4d7SAndreas Gohr        if (!$token->getRefreshToken()) {
1629cbef4d7SAndreas Gohr            $token->setRefreshToken($refreshToken);
1639cbef4d7SAndreas Gohr            $oauth->getStorage()->storeAccessToken($oauth->service(), $token);
1649cbef4d7SAndreas Gohr        }
1659cbef4d7SAndreas Gohr    }
1669cbef4d7SAndreas Gohr
1679cbef4d7SAndreas Gohr    /**
16804a78b87SAndreas Gohr     * Redirects to the service for requesting access
16904a78b87SAndreas Gohr     *
17004a78b87SAndreas Gohr     * This is the first step of oAuth authentication
17104a78b87SAndreas Gohr     *
17204a78b87SAndreas Gohr     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
17304a78b87SAndreas Gohr     * but might need to be overwritten for specific services
17404a78b87SAndreas Gohr     *
17504a78b87SAndreas Gohr     * @throws TokenResponseException
176*290e9b1fSAndreas Gohr     * @throws \Exception
17704a78b87SAndreas Gohr     */
17804a78b87SAndreas Gohr    public function login()
17904a78b87SAndreas Gohr    {
18004a78b87SAndreas Gohr        $oauth = $this->getOAuthService();
18104a78b87SAndreas Gohr
18204a78b87SAndreas Gohr        // store Farmer animal in oAuth state parameter
18304a78b87SAndreas Gohr        /** @var \helper_plugin_farmer $farmer */
18404a78b87SAndreas Gohr        $farmer = plugin_load('helper', 'farmer');
18504a78b87SAndreas Gohr        $parameters = [];
18604a78b87SAndreas Gohr        if ($farmer && $animal = $farmer->getAnimal()) {
18704a78b87SAndreas Gohr            $parameters['state'] = urlencode(base64_encode(json_encode(
18804a78b87SAndreas Gohr                [
18904a78b87SAndreas Gohr                    'animal' => $animal,
190*290e9b1fSAndreas Gohr                    'state' => md5(random_int(0, mt_getrandmax())),
19104a78b87SAndreas Gohr                ]
19204a78b87SAndreas Gohr            )));
19304a78b87SAndreas Gohr            $oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']);
19404a78b87SAndreas Gohr        }
19504a78b87SAndreas Gohr
19604a78b87SAndreas Gohr        if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */
19704a78b87SAndreas Gohr            // extra request needed for oauth1 to request a request token
19804a78b87SAndreas Gohr            $token = $oauth->requestRequestToken();
19904a78b87SAndreas Gohr            $parameters['oauth_token'] = $token->getRequestToken();
20004a78b87SAndreas Gohr        }
20104a78b87SAndreas Gohr        $url = $oauth->getAuthorizationUri($parameters);
20204a78b87SAndreas Gohr
20304a78b87SAndreas Gohr        send_redirect($url);
20404a78b87SAndreas Gohr    }
20504a78b87SAndreas Gohr
20604a78b87SAndreas Gohr    /**
20704a78b87SAndreas Gohr     * Request access token
20804a78b87SAndreas Gohr     *
20904a78b87SAndreas Gohr     * This is the second step of oAuth authentication
21004a78b87SAndreas Gohr     *
21104a78b87SAndreas Gohr     * This implementation tries to abstract away differences between oAuth1 and oAuth2,
21204a78b87SAndreas Gohr     * but might need to be overwritten for specific services
21304a78b87SAndreas Gohr     *
21404a78b87SAndreas Gohr     * Thrown exceptions indicate a non-successful login because of some error, appropriate messages
21504a78b87SAndreas Gohr     * should be shown to the user. A return of false with no exceptions indicates that there was no
21604a78b87SAndreas Gohr     * oauth data at all. This can probably be silently ignored.
21704a78b87SAndreas Gohr     *
21804a78b87SAndreas Gohr     * @return bool true if authentication was successful
21904a78b87SAndreas Gohr     * @throws \OAuth\Common\Exception\Exception
22004a78b87SAndreas Gohr     * @throws InvalidAuthorizationStateException
22104a78b87SAndreas Gohr     */
22204a78b87SAndreas Gohr    public function checkToken()
22304a78b87SAndreas Gohr    {
22404a78b87SAndreas Gohr        global $INPUT;
22504a78b87SAndreas Gohr
22604a78b87SAndreas Gohr        $oauth = $this->getOAuthService();
22704a78b87SAndreas Gohr
22804a78b87SAndreas Gohr        if (is_a($oauth, Abstract2Service::class)) {
22904a78b87SAndreas Gohr            if (!$INPUT->get->has('code')) return false;
23004a78b87SAndreas Gohr            $state = $INPUT->get->str('state', null);
231eae50416SAndreas Gohr            $accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state);
23204a78b87SAndreas Gohr        } else {
23304a78b87SAndreas Gohr            if (!$INPUT->get->has('oauth_token')) return false;
23404a78b87SAndreas Gohr            /** @var TokenInterface $token */
23504a78b87SAndreas Gohr            $token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID());
236eae50416SAndreas Gohr            $accessToken = $oauth->requestAccessToken(
23704a78b87SAndreas Gohr                $INPUT->get->str('oauth_token'),
23804a78b87SAndreas Gohr                $INPUT->get->str('oauth_verifier'),
23904a78b87SAndreas Gohr                $token->getRequestTokenSecret()
24004a78b87SAndreas Gohr            );
24104a78b87SAndreas Gohr        }
242eae50416SAndreas Gohr
243eae50416SAndreas Gohr        if (
244eae50416SAndreas Gohr            $accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES &&
245*290e9b1fSAndreas Gohr            !$accessToken->getRefreshToken()
246*290e9b1fSAndreas Gohr        ) {
247eae50416SAndreas Gohr            msg('Service did not provide a Refresh Token. You will be logged out when the session expires.');
248eae50416SAndreas Gohr        }
249eae50416SAndreas Gohr
25004a78b87SAndreas Gohr        return true;
25104a78b87SAndreas Gohr    }
25204a78b87SAndreas Gohr
25304a78b87SAndreas Gohr    /**
25404a78b87SAndreas Gohr     * Return the Service Login Button
25504a78b87SAndreas Gohr     *
25604a78b87SAndreas Gohr     * @return string
25704a78b87SAndreas Gohr     */
25804a78b87SAndreas Gohr    public function loginButton()
25904a78b87SAndreas Gohr    {
26004a78b87SAndreas Gohr        global $ID;
26104a78b87SAndreas Gohr
26204a78b87SAndreas Gohr        $attr = buildAttributes([
263*290e9b1fSAndreas Gohr            'href' => wl($ID, ['oauthlogin' => $this->getServiceID()], false, '&'),
26404a78b87SAndreas Gohr            'class' => 'plugin_oauth_' . $this->getServiceID(),
26504a78b87SAndreas Gohr            'style' => 'background-color: ' . $this->getColor(),
26604a78b87SAndreas Gohr        ]);
26704a78b87SAndreas Gohr
26804a78b87SAndreas Gohr        return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> ';
26904a78b87SAndreas Gohr    }
27004a78b87SAndreas Gohr    // endregion
27104a78b87SAndreas Gohr
27204a78b87SAndreas Gohr    // region overridable methods
27304a78b87SAndreas Gohr
27404a78b87SAndreas Gohr    /**
275a1fa007aSNaoto Kobayashi     * Called on logout
276a1fa007aSNaoto Kobayashi     *
277a1fa007aSNaoto Kobayashi     * If there are required procedures for the service, you can implement them by overriding this.
278a1fa007aSNaoto Kobayashi     *
279a1fa007aSNaoto Kobayashi     * @return void
280a1fa007aSNaoto Kobayashi     */
281a1fa007aSNaoto Kobayashi    public function logout()
282a1fa007aSNaoto Kobayashi    {
283a1fa007aSNaoto Kobayashi    }
284a1fa007aSNaoto Kobayashi
285a1fa007aSNaoto Kobayashi    /**
28604a78b87SAndreas Gohr     * Retrieve the user's data via API
28704a78b87SAndreas Gohr     *
28804a78b87SAndreas Gohr     * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps'
28904a78b87SAndreas Gohr     *
29004a78b87SAndreas Gohr     * Use the request() method of the oauth object to talk to the API
29104a78b87SAndreas Gohr     *
29204a78b87SAndreas Gohr     * @return array
29304a78b87SAndreas Gohr     * @throws Exception
29404a78b87SAndreas Gohr     * @see getOAuthService()
29504a78b87SAndreas Gohr     */
29604a78b87SAndreas Gohr    abstract public function getUser();
29704a78b87SAndreas Gohr
29804a78b87SAndreas Gohr    /**
29904a78b87SAndreas Gohr     * Return the scopes to request
30004a78b87SAndreas Gohr     *
30104a78b87SAndreas Gohr     * This should return the minimal scopes needed for accessing the user's data
30204a78b87SAndreas Gohr     *
30304a78b87SAndreas Gohr     * @return string[]
30404a78b87SAndreas Gohr     */
30504a78b87SAndreas Gohr    public function getScopes()
30604a78b87SAndreas Gohr    {
30704a78b87SAndreas Gohr        return [];
30804a78b87SAndreas Gohr    }
30904a78b87SAndreas Gohr
31004a78b87SAndreas Gohr    /**
31104a78b87SAndreas Gohr     * Return the user friendly name of the service
31204a78b87SAndreas Gohr     *
31304a78b87SAndreas Gohr     * Defaults to ServiceID. You may want to override this.
31404a78b87SAndreas Gohr     *
31504a78b87SAndreas Gohr     * @return string
31604a78b87SAndreas Gohr     */
31704a78b87SAndreas Gohr    public function getLabel()
31804a78b87SAndreas Gohr    {
31904a78b87SAndreas Gohr        return ucfirst($this->getServiceID());
32004a78b87SAndreas Gohr    }
32104a78b87SAndreas Gohr
32204a78b87SAndreas Gohr    /**
32304a78b87SAndreas Gohr     * Return the internal name of the Service
32404a78b87SAndreas Gohr     *
32504a78b87SAndreas Gohr     * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in
32604a78b87SAndreas Gohr     * the appropriate lusitantian oauth Service namespace
32704a78b87SAndreas Gohr     *
32804a78b87SAndreas Gohr     * @return string
32904a78b87SAndreas Gohr     */
33004a78b87SAndreas Gohr    public function getServiceID()
33104a78b87SAndreas Gohr    {
33204a78b87SAndreas Gohr        $name = $this->getPluginName();
33304a78b87SAndreas Gohr        if (substr($name, 0, 5) === 'oauth') {
33404a78b87SAndreas Gohr            $name = substr($name, 5);
33504a78b87SAndreas Gohr        }
33604a78b87SAndreas Gohr
33704a78b87SAndreas Gohr        return $name;
33804a78b87SAndreas Gohr    }
33904a78b87SAndreas Gohr
34004a78b87SAndreas Gohr    /**
34104a78b87SAndreas Gohr     * Register a new Service
34204a78b87SAndreas Gohr     *
34304a78b87SAndreas Gohr     * @return string A fully qualified class name to register as new Service for your ServiceID
34404a78b87SAndreas Gohr     */
34504a78b87SAndreas Gohr    public function registerServiceClass()
34604a78b87SAndreas Gohr    {
34704a78b87SAndreas Gohr        return null;
34804a78b87SAndreas Gohr    }
34904a78b87SAndreas Gohr
35004a78b87SAndreas Gohr    /**
35104a78b87SAndreas Gohr     * Return the button color to use
35204a78b87SAndreas Gohr     *
35304a78b87SAndreas Gohr     * @return string
35404a78b87SAndreas Gohr     */
35504a78b87SAndreas Gohr    public function getColor()
35604a78b87SAndreas Gohr    {
35704a78b87SAndreas Gohr        return '#999';
35804a78b87SAndreas Gohr    }
35904a78b87SAndreas Gohr
36004a78b87SAndreas Gohr    /**
36104a78b87SAndreas Gohr     * Return the SVG of the logo for this service
36204a78b87SAndreas Gohr     *
36304a78b87SAndreas Gohr     * Defaults to a logo.svg in the plugin directory
36404a78b87SAndreas Gohr     *
36504a78b87SAndreas Gohr     * @return string
36604a78b87SAndreas Gohr     */
36704a78b87SAndreas Gohr    public function getSvgLogo()
36804a78b87SAndreas Gohr    {
36904a78b87SAndreas Gohr        $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg';
37004a78b87SAndreas Gohr        if (file_exists($logo)) return inlineSVG($logo);
37104a78b87SAndreas Gohr        return '';
37204a78b87SAndreas Gohr    }
37304a78b87SAndreas Gohr
37404a78b87SAndreas Gohr    /**
37504a78b87SAndreas Gohr     * The oauth key
37604a78b87SAndreas Gohr     *
37704a78b87SAndreas Gohr     * @return string
37804a78b87SAndreas Gohr     */
37904a78b87SAndreas Gohr    public function getKey()
38004a78b87SAndreas Gohr    {
38104a78b87SAndreas Gohr        return $this->getConf('key');
38204a78b87SAndreas Gohr    }
38304a78b87SAndreas Gohr
38404a78b87SAndreas Gohr    /**
38504a78b87SAndreas Gohr     * The oauth secret
38604a78b87SAndreas Gohr     *
38704a78b87SAndreas Gohr     * @return string
38804a78b87SAndreas Gohr     */
38904a78b87SAndreas Gohr    public function getSecret()
39004a78b87SAndreas Gohr    {
39104a78b87SAndreas Gohr        return $this->getConf('secret');
39204a78b87SAndreas Gohr    }
39304a78b87SAndreas Gohr
39404a78b87SAndreas Gohr    // endregion
39504a78b87SAndreas Gohr}
396