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