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; 928002081SAndreas 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 * 5128002081SAndreas Gohr * @param string $storageId user based storage key (if available, yet) 5204a78b87SAndreas Gohr * @throws \OAuth\Common\Exception\Exception 5304a78b87SAndreas Gohr */ 5428002081SAndreas 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 7328002081SAndreas Gohr if ($storageId) { 7428002081SAndreas Gohr $storage = new Storage($storageId); 7528002081SAndreas Gohr } else { 7628002081SAndreas Gohr $storage = new SessionStorage(); 7728002081SAndreas Gohr } 7828002081SAndreas Gohr 7904a78b87SAndreas Gohr $this->oAuth = $serviceFactory->createService( 8004a78b87SAndreas Gohr $servicename, 8104a78b87SAndreas Gohr $credentials, 8228002081SAndreas 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 /** 10228002081SAndreas Gohr * Once a user has been authenticated, the current token storage needs to be made permanent 10328002081SAndreas Gohr * 10428002081SAndreas Gohr * @param string $storageId 10528002081SAndreas Gohr * @throws Exception 10628002081SAndreas Gohr * @throws TokenNotFoundException 10728002081SAndreas Gohr */ 10828002081SAndreas Gohr public function upgradeStorage($storageId) 10928002081SAndreas Gohr { 11028002081SAndreas Gohr $oauth = $this->getOAuthService(); 11128002081SAndreas Gohr $service = $oauth->service(); 11228002081SAndreas Gohr 11328002081SAndreas Gohr $oldStorage = $oauth->getStorage(); 11428002081SAndreas Gohr $newStorage = new Storage($storageId); 11528002081SAndreas Gohr if ($oldStorage->hasAccessToken($service)) { 11628002081SAndreas Gohr $newStorage->storeAccessToken($service, $oldStorage->retrieveAccessToken($service)); 11728002081SAndreas Gohr } 11828002081SAndreas Gohr if ($oldStorage->hasAuthorizationState($service)) { 11928002081SAndreas Gohr $newStorage->storeAuthorizationState($service, $oldStorage->retrieveAuthorizationState($service)); 12028002081SAndreas Gohr } 12128002081SAndreas Gohr 12228002081SAndreas Gohr // fixme invalidate current oauth object? reinitialize it? 12328002081SAndreas Gohr } 12428002081SAndreas Gohr 12528002081SAndreas 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 /** 271*a1fa007aSNaoto Kobayashi * Called on logout 272*a1fa007aSNaoto Kobayashi * 273*a1fa007aSNaoto Kobayashi * If there are required procedures for the service, you can implement them by overriding this. 274*a1fa007aSNaoto Kobayashi * 275*a1fa007aSNaoto Kobayashi * @return void 276*a1fa007aSNaoto Kobayashi */ 277*a1fa007aSNaoto Kobayashi public function logout() 278*a1fa007aSNaoto Kobayashi { 279*a1fa007aSNaoto Kobayashi } 280*a1fa007aSNaoto Kobayashi 281*a1fa007aSNaoto Kobayashi /** 28204a78b87SAndreas Gohr * Retrieve the user's data via API 28304a78b87SAndreas Gohr * 28404a78b87SAndreas Gohr * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps' 28504a78b87SAndreas Gohr * 28604a78b87SAndreas Gohr * Use the request() method of the oauth object to talk to the API 28704a78b87SAndreas Gohr * 28804a78b87SAndreas Gohr * @return array 28904a78b87SAndreas Gohr * @throws Exception 29004a78b87SAndreas Gohr * @see getOAuthService() 29104a78b87SAndreas Gohr */ 29204a78b87SAndreas Gohr abstract public function getUser(); 29304a78b87SAndreas Gohr 29404a78b87SAndreas Gohr /** 29504a78b87SAndreas Gohr * Return the scopes to request 29604a78b87SAndreas Gohr * 29704a78b87SAndreas Gohr * This should return the minimal scopes needed for accessing the user's data 29804a78b87SAndreas Gohr * 29904a78b87SAndreas Gohr * @return string[] 30004a78b87SAndreas Gohr */ 30104a78b87SAndreas Gohr public function getScopes() 30204a78b87SAndreas Gohr { 30304a78b87SAndreas Gohr return []; 30404a78b87SAndreas Gohr } 30504a78b87SAndreas Gohr 30604a78b87SAndreas Gohr /** 30704a78b87SAndreas Gohr * Return the user friendly name of the service 30804a78b87SAndreas Gohr * 30904a78b87SAndreas Gohr * Defaults to ServiceID. You may want to override this. 31004a78b87SAndreas Gohr * 31104a78b87SAndreas Gohr * @return string 31204a78b87SAndreas Gohr */ 31304a78b87SAndreas Gohr public function getLabel() 31404a78b87SAndreas Gohr { 31504a78b87SAndreas Gohr return ucfirst($this->getServiceID()); 31604a78b87SAndreas Gohr } 31704a78b87SAndreas Gohr 31804a78b87SAndreas Gohr /** 31904a78b87SAndreas Gohr * Return the internal name of the Service 32004a78b87SAndreas Gohr * 32104a78b87SAndreas Gohr * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in 32204a78b87SAndreas Gohr * the appropriate lusitantian oauth Service namespace 32304a78b87SAndreas Gohr * 32404a78b87SAndreas Gohr * @return string 32504a78b87SAndreas Gohr */ 32604a78b87SAndreas Gohr public function getServiceID() 32704a78b87SAndreas Gohr { 32804a78b87SAndreas Gohr $name = $this->getPluginName(); 32904a78b87SAndreas Gohr if (substr($name, 0, 5) === 'oauth') { 33004a78b87SAndreas Gohr $name = substr($name, 5); 33104a78b87SAndreas Gohr } 33204a78b87SAndreas Gohr 33304a78b87SAndreas Gohr return $name; 33404a78b87SAndreas Gohr } 33504a78b87SAndreas Gohr 33604a78b87SAndreas Gohr /** 33704a78b87SAndreas Gohr * Register a new Service 33804a78b87SAndreas Gohr * 33904a78b87SAndreas Gohr * @return string A fully qualified class name to register as new Service for your ServiceID 34004a78b87SAndreas Gohr */ 34104a78b87SAndreas Gohr public function registerServiceClass() 34204a78b87SAndreas Gohr { 34304a78b87SAndreas Gohr return null; 34404a78b87SAndreas Gohr } 34504a78b87SAndreas Gohr 34604a78b87SAndreas Gohr /** 34704a78b87SAndreas Gohr * Return the button color to use 34804a78b87SAndreas Gohr * 34904a78b87SAndreas Gohr * @return string 35004a78b87SAndreas Gohr */ 35104a78b87SAndreas Gohr public function getColor() 35204a78b87SAndreas Gohr { 35304a78b87SAndreas Gohr return '#999'; 35404a78b87SAndreas Gohr } 35504a78b87SAndreas Gohr 35604a78b87SAndreas Gohr /** 35704a78b87SAndreas Gohr * Return the SVG of the logo for this service 35804a78b87SAndreas Gohr * 35904a78b87SAndreas Gohr * Defaults to a logo.svg in the plugin directory 36004a78b87SAndreas Gohr * 36104a78b87SAndreas Gohr * @return string 36204a78b87SAndreas Gohr */ 36304a78b87SAndreas Gohr public function getSvgLogo() 36404a78b87SAndreas Gohr { 36504a78b87SAndreas Gohr $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg'; 36604a78b87SAndreas Gohr if (file_exists($logo)) return inlineSVG($logo); 36704a78b87SAndreas Gohr return ''; 36804a78b87SAndreas Gohr } 36904a78b87SAndreas Gohr 37004a78b87SAndreas Gohr /** 37104a78b87SAndreas Gohr * The oauth key 37204a78b87SAndreas Gohr * 37304a78b87SAndreas Gohr * @return string 37404a78b87SAndreas Gohr */ 37504a78b87SAndreas Gohr public function getKey() 37604a78b87SAndreas Gohr { 37704a78b87SAndreas Gohr return $this->getConf('key'); 37804a78b87SAndreas Gohr } 37904a78b87SAndreas Gohr 38004a78b87SAndreas Gohr /** 38104a78b87SAndreas Gohr * The oauth secret 38204a78b87SAndreas Gohr * 38304a78b87SAndreas Gohr * @return string 38404a78b87SAndreas Gohr */ 38504a78b87SAndreas Gohr public function getSecret() 38604a78b87SAndreas Gohr { 38704a78b87SAndreas Gohr return $this->getConf('secret'); 38804a78b87SAndreas Gohr } 38904a78b87SAndreas Gohr 39004a78b87SAndreas Gohr // endregion 39104a78b87SAndreas Gohr} 392