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; 8*9cbef4d7SAndreas Gohruse OAuth\Common\Storage\Exception\TokenNotFoundException; 904a78b87SAndreas Gohruse OAuth\OAuth1\Service\AbstractService as Abstract1Service; 1004a78b87SAndreas Gohruse OAuth\OAuth1\Token\TokenInterface; 1104a78b87SAndreas Gohruse OAuth\OAuth2\Service\AbstractService as Abstract2Service; 1204a78b87SAndreas Gohruse OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException; 13*9cbef4d7SAndreas Gohruse OAuth\OAuth2\Service\Exception\MissingRefreshTokenException; 1404a78b87SAndreas Gohruse OAuth\ServiceFactory; 1504a78b87SAndreas Gohr 1604a78b87SAndreas Gohr/** 1704a78b87SAndreas Gohr * Base class to implement a Backend Service for the oAuth Plugin 1804a78b87SAndreas Gohr */ 1904a78b87SAndreas Gohrabstract class Adapter extends ActionPlugin 2004a78b87SAndreas Gohr{ 2104a78b87SAndreas Gohr /** 2204a78b87SAndreas Gohr * @var Abstract2Service|Abstract1Service 2304a78b87SAndreas Gohr * @see getOAuthService() use this to ensure it's intialized 2404a78b87SAndreas Gohr */ 2504a78b87SAndreas Gohr protected $oAuth; 2604a78b87SAndreas Gohr 2704a78b87SAndreas Gohr // region internal methods 2804a78b87SAndreas Gohr 2904a78b87SAndreas Gohr /** 3004a78b87SAndreas Gohr * Auto register this plugin with the oAuth authentication plugin 3104a78b87SAndreas Gohr * 3204a78b87SAndreas Gohr * @inheritDoc 3304a78b87SAndreas Gohr */ 3404a78b87SAndreas Gohr public function register(\Doku_Event_Handler $controller) 3504a78b87SAndreas Gohr { 3604a78b87SAndreas Gohr $controller->register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister'); 3704a78b87SAndreas Gohr } 3804a78b87SAndreas Gohr 3904a78b87SAndreas Gohr /** 4004a78b87SAndreas Gohr * Auto register this plugin with the oAuth authentication plugin 4104a78b87SAndreas Gohr */ 4204a78b87SAndreas Gohr public function handleRegister(\Doku_Event $event, $param) 4304a78b87SAndreas Gohr { 4404a78b87SAndreas Gohr $event->data[$this->getServiceID()] = $this; 4504a78b87SAndreas Gohr } 4604a78b87SAndreas Gohr 4704a78b87SAndreas Gohr /** 4804a78b87SAndreas Gohr * Initialize the oAuth service 4904a78b87SAndreas Gohr * 5004a78b87SAndreas Gohr * @param string $guid UIID for the user to authenticate 5104a78b87SAndreas Gohr * @throws \OAuth\Common\Exception\Exception 5204a78b87SAndreas Gohr */ 5304a78b87SAndreas Gohr public function initOAuthService($guid) 5404a78b87SAndreas Gohr { 5504a78b87SAndreas Gohr /** @var \helper_plugin_oauth $hlp */ 5604a78b87SAndreas Gohr $hlp = plugin_load('helper', 'oauth'); 5704a78b87SAndreas Gohr 5804a78b87SAndreas Gohr $credentials = new Credentials( 5904a78b87SAndreas Gohr $this->getKey(), 6004a78b87SAndreas Gohr $this->getSecret(), 6104a78b87SAndreas Gohr $hlp->redirectURI() 6204a78b87SAndreas Gohr ); 6304a78b87SAndreas Gohr 6404a78b87SAndreas Gohr $serviceFactory = new ServiceFactory(); 6504a78b87SAndreas Gohr $serviceFactory->setHttpClient(new HTTPClient()); 6604a78b87SAndreas Gohr $servicename = $this->getServiceID(); 6704a78b87SAndreas Gohr $serviceclass = $this->registerServiceClass(); 6804a78b87SAndreas Gohr if ($serviceclass) { 6904a78b87SAndreas Gohr $serviceFactory->registerService($servicename, $serviceclass); 7004a78b87SAndreas Gohr } 7104a78b87SAndreas Gohr 7204a78b87SAndreas Gohr $this->oAuth = $serviceFactory->createService( 7304a78b87SAndreas Gohr $servicename, 7404a78b87SAndreas Gohr $credentials, 7504a78b87SAndreas Gohr new Storage($guid), 7604a78b87SAndreas Gohr $this->getScopes() 7704a78b87SAndreas Gohr ); 7804a78b87SAndreas Gohr 7904a78b87SAndreas Gohr if ($this->oAuth === null) { 8004a78b87SAndreas Gohr throw new Exception('Failed to initialize Service ' . $this->getLabel()); 8104a78b87SAndreas Gohr } 8204a78b87SAndreas Gohr } 8304a78b87SAndreas Gohr 8404a78b87SAndreas Gohr /** 8504a78b87SAndreas Gohr * @return Abstract2Service|Abstract1Service 8604a78b87SAndreas Gohr * @throws Exception 8704a78b87SAndreas Gohr */ 8804a78b87SAndreas Gohr public function getOAuthService() 8904a78b87SAndreas Gohr { 9004a78b87SAndreas Gohr if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized'); 9104a78b87SAndreas Gohr return $this->oAuth; 9204a78b87SAndreas Gohr } 9304a78b87SAndreas Gohr 9404a78b87SAndreas Gohr /** 95*9cbef4d7SAndreas Gohr * Refresh a possibly outdated access token 96*9cbef4d7SAndreas Gohr * 97*9cbef4d7SAndreas Gohr * Does nothing when the current token is still good to use 98*9cbef4d7SAndreas Gohr * 99*9cbef4d7SAndreas Gohr * @return void 100*9cbef4d7SAndreas Gohr * @throws MissingRefreshTokenException 101*9cbef4d7SAndreas Gohr * @throws TokenNotFoundException 102*9cbef4d7SAndreas Gohr * @throws TokenResponseException 103*9cbef4d7SAndreas Gohr * @throws Exception 104*9cbef4d7SAndreas Gohr */ 105*9cbef4d7SAndreas Gohr public function refreshOutdatedToken() 106*9cbef4d7SAndreas Gohr { 107*9cbef4d7SAndreas Gohr $oauth = $this->getOAuthService(); 108*9cbef4d7SAndreas Gohr 109*9cbef4d7SAndreas Gohr if (!$oauth->getStorage()->hasAccessToken($oauth->service())) { 110*9cbef4d7SAndreas Gohr // no token to refresh 111*9cbef4d7SAndreas Gohr return; 112*9cbef4d7SAndreas Gohr } 113*9cbef4d7SAndreas Gohr 114*9cbef4d7SAndreas Gohr $token = $oauth->getStorage()->retrieveAccessToken($oauth->service()); 115*9cbef4d7SAndreas Gohr if ($token->getEndOfLife() < 0 || 116*9cbef4d7SAndreas Gohr $token->getEndOfLife() - time() > 3600) { 117*9cbef4d7SAndreas Gohr // token is still good 118*9cbef4d7SAndreas Gohr return; 119*9cbef4d7SAndreas Gohr } 120*9cbef4d7SAndreas Gohr 121*9cbef4d7SAndreas Gohr $refreshToken = $token->getRefreshToken(); 122*9cbef4d7SAndreas Gohr $token = $oauth->refreshAccessToken($token); 123*9cbef4d7SAndreas Gohr 124*9cbef4d7SAndreas Gohr // If the IDP did not provide a new refresh token, store the old one 125*9cbef4d7SAndreas Gohr if (!$token->getRefreshToken()) { 126*9cbef4d7SAndreas Gohr $token->setRefreshToken($refreshToken); 127*9cbef4d7SAndreas Gohr $oauth->getStorage()->storeAccessToken($oauth->service(), $token); 128*9cbef4d7SAndreas Gohr } 129*9cbef4d7SAndreas Gohr } 130*9cbef4d7SAndreas Gohr 131*9cbef4d7SAndreas Gohr /** 13204a78b87SAndreas Gohr * Redirects to the service for requesting access 13304a78b87SAndreas Gohr * 13404a78b87SAndreas Gohr * This is the first 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 * @throws TokenResponseException 14004a78b87SAndreas Gohr * @throws Exception 14104a78b87SAndreas Gohr */ 14204a78b87SAndreas Gohr public function login() 14304a78b87SAndreas Gohr { 14404a78b87SAndreas Gohr $oauth = $this->getOAuthService(); 14504a78b87SAndreas Gohr 14604a78b87SAndreas Gohr // store Farmer animal in oAuth state parameter 14704a78b87SAndreas Gohr /** @var \helper_plugin_farmer $farmer */ 14804a78b87SAndreas Gohr $farmer = plugin_load('helper', 'farmer'); 14904a78b87SAndreas Gohr $parameters = []; 15004a78b87SAndreas Gohr if ($farmer && $animal = $farmer->getAnimal()) { 15104a78b87SAndreas Gohr $parameters['state'] = urlencode(base64_encode(json_encode( 15204a78b87SAndreas Gohr [ 15304a78b87SAndreas Gohr 'animal' => $animal, 15404a78b87SAndreas Gohr 'state' => md5(rand()), 15504a78b87SAndreas Gohr ] 15604a78b87SAndreas Gohr ))); 15704a78b87SAndreas Gohr $oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']); 15804a78b87SAndreas Gohr } 15904a78b87SAndreas Gohr 16004a78b87SAndreas Gohr if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */ 16104a78b87SAndreas Gohr // extra request needed for oauth1 to request a request token 16204a78b87SAndreas Gohr $token = $oauth->requestRequestToken(); 16304a78b87SAndreas Gohr $parameters['oauth_token'] = $token->getRequestToken(); 16404a78b87SAndreas Gohr } 16504a78b87SAndreas Gohr $url = $oauth->getAuthorizationUri($parameters); 16604a78b87SAndreas Gohr 16704a78b87SAndreas Gohr send_redirect($url); 16804a78b87SAndreas Gohr } 16904a78b87SAndreas Gohr 17004a78b87SAndreas Gohr /** 17104a78b87SAndreas Gohr * Request access token 17204a78b87SAndreas Gohr * 17304a78b87SAndreas Gohr * This is the second step of oAuth authentication 17404a78b87SAndreas Gohr * 17504a78b87SAndreas Gohr * This implementation tries to abstract away differences between oAuth1 and oAuth2, 17604a78b87SAndreas Gohr * but might need to be overwritten for specific services 17704a78b87SAndreas Gohr * 17804a78b87SAndreas Gohr * Thrown exceptions indicate a non-successful login because of some error, appropriate messages 17904a78b87SAndreas Gohr * should be shown to the user. A return of false with no exceptions indicates that there was no 18004a78b87SAndreas Gohr * oauth data at all. This can probably be silently ignored. 18104a78b87SAndreas Gohr * 18204a78b87SAndreas Gohr * @return bool true if authentication was successful 18304a78b87SAndreas Gohr * @throws \OAuth\Common\Exception\Exception 18404a78b87SAndreas Gohr * @throws InvalidAuthorizationStateException 18504a78b87SAndreas Gohr */ 18604a78b87SAndreas Gohr public function checkToken() 18704a78b87SAndreas Gohr { 18804a78b87SAndreas Gohr global $INPUT; 18904a78b87SAndreas Gohr 19004a78b87SAndreas Gohr $oauth = $this->getOAuthService(); 19104a78b87SAndreas Gohr 19204a78b87SAndreas Gohr if (is_a($oauth, Abstract2Service::class)) { 19304a78b87SAndreas Gohr /** @var Abstract2Service $oauth */ 19404a78b87SAndreas Gohr if (!$INPUT->get->has('code')) return false; 19504a78b87SAndreas Gohr $state = $INPUT->get->str('state', null); 196eae50416SAndreas Gohr $accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state); 19704a78b87SAndreas Gohr } else { 19804a78b87SAndreas Gohr /** @var Abstract1Service $oauth */ 19904a78b87SAndreas Gohr if (!$INPUT->get->has('oauth_token')) return false; 20004a78b87SAndreas Gohr /** @var TokenInterface $token */ 20104a78b87SAndreas Gohr $token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID()); 202eae50416SAndreas Gohr $accessToken = $oauth->requestAccessToken( 20304a78b87SAndreas Gohr $INPUT->get->str('oauth_token'), 20404a78b87SAndreas Gohr $INPUT->get->str('oauth_verifier'), 20504a78b87SAndreas Gohr $token->getRequestTokenSecret() 20604a78b87SAndreas Gohr ); 20704a78b87SAndreas Gohr } 208eae50416SAndreas Gohr 209eae50416SAndreas Gohr if ( 210eae50416SAndreas Gohr $accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES && 211eae50416SAndreas Gohr !$accessToken->getRefreshToken()) { 212eae50416SAndreas Gohr msg('Service did not provide a Refresh Token. You will be logged out when the session expires.'); 213eae50416SAndreas Gohr } 214eae50416SAndreas Gohr 21504a78b87SAndreas Gohr return true; 21604a78b87SAndreas Gohr } 21704a78b87SAndreas Gohr 21804a78b87SAndreas Gohr /** 21904a78b87SAndreas Gohr * Return the Service Login Button 22004a78b87SAndreas Gohr * 22104a78b87SAndreas Gohr * @return string 22204a78b87SAndreas Gohr */ 22304a78b87SAndreas Gohr public function loginButton() 22404a78b87SAndreas Gohr { 22504a78b87SAndreas Gohr global $ID; 22604a78b87SAndreas Gohr 22704a78b87SAndreas Gohr $attr = buildAttributes([ 22804a78b87SAndreas Gohr 'href' => wl($ID, array('oauthlogin' => $this->getServiceID()), false, '&'), 22904a78b87SAndreas Gohr 'class' => 'plugin_oauth_' . $this->getServiceID(), 23004a78b87SAndreas Gohr 'style' => 'background-color: ' . $this->getColor(), 23104a78b87SAndreas Gohr ]); 23204a78b87SAndreas Gohr 23304a78b87SAndreas Gohr return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> '; 23404a78b87SAndreas Gohr } 23504a78b87SAndreas Gohr // endregion 23604a78b87SAndreas Gohr 23704a78b87SAndreas Gohr // region overridable methods 23804a78b87SAndreas Gohr 23904a78b87SAndreas Gohr /** 24004a78b87SAndreas Gohr * Retrieve the user's data via API 24104a78b87SAndreas Gohr * 24204a78b87SAndreas Gohr * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps' 24304a78b87SAndreas Gohr * 24404a78b87SAndreas Gohr * Use the request() method of the oauth object to talk to the API 24504a78b87SAndreas Gohr * 24604a78b87SAndreas Gohr * @return array 24704a78b87SAndreas Gohr * @throws Exception 24804a78b87SAndreas Gohr * @see getOAuthService() 24904a78b87SAndreas Gohr */ 25004a78b87SAndreas Gohr abstract public function getUser(); 25104a78b87SAndreas Gohr 25204a78b87SAndreas Gohr /** 25304a78b87SAndreas Gohr * Return the scopes to request 25404a78b87SAndreas Gohr * 25504a78b87SAndreas Gohr * This should return the minimal scopes needed for accessing the user's data 25604a78b87SAndreas Gohr * 25704a78b87SAndreas Gohr * @return string[] 25804a78b87SAndreas Gohr */ 25904a78b87SAndreas Gohr public function getScopes() 26004a78b87SAndreas Gohr { 26104a78b87SAndreas Gohr return []; 26204a78b87SAndreas Gohr } 26304a78b87SAndreas Gohr 26404a78b87SAndreas Gohr /** 26504a78b87SAndreas Gohr * Return the user friendly name of the service 26604a78b87SAndreas Gohr * 26704a78b87SAndreas Gohr * Defaults to ServiceID. You may want to override this. 26804a78b87SAndreas Gohr * 26904a78b87SAndreas Gohr * @return string 27004a78b87SAndreas Gohr */ 27104a78b87SAndreas Gohr public function getLabel() 27204a78b87SAndreas Gohr { 27304a78b87SAndreas Gohr return ucfirst($this->getServiceID()); 27404a78b87SAndreas Gohr } 27504a78b87SAndreas Gohr 27604a78b87SAndreas Gohr /** 27704a78b87SAndreas Gohr * Return the internal name of the Service 27804a78b87SAndreas Gohr * 27904a78b87SAndreas Gohr * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in 28004a78b87SAndreas Gohr * the appropriate lusitantian oauth Service namespace 28104a78b87SAndreas Gohr * 28204a78b87SAndreas Gohr * @return string 28304a78b87SAndreas Gohr */ 28404a78b87SAndreas Gohr public function getServiceID() 28504a78b87SAndreas Gohr { 28604a78b87SAndreas Gohr $name = $this->getPluginName(); 28704a78b87SAndreas Gohr if (substr($name, 0, 5) === 'oauth') { 28804a78b87SAndreas Gohr $name = substr($name, 5); 28904a78b87SAndreas Gohr } 29004a78b87SAndreas Gohr 29104a78b87SAndreas Gohr return $name; 29204a78b87SAndreas Gohr } 29304a78b87SAndreas Gohr 29404a78b87SAndreas Gohr /** 29504a78b87SAndreas Gohr * Register a new Service 29604a78b87SAndreas Gohr * 29704a78b87SAndreas Gohr * @return string A fully qualified class name to register as new Service for your ServiceID 29804a78b87SAndreas Gohr */ 29904a78b87SAndreas Gohr public function registerServiceClass() 30004a78b87SAndreas Gohr { 30104a78b87SAndreas Gohr return null; 30204a78b87SAndreas Gohr } 30304a78b87SAndreas Gohr 30404a78b87SAndreas Gohr /** 30504a78b87SAndreas Gohr * Return the button color to use 30604a78b87SAndreas Gohr * 30704a78b87SAndreas Gohr * @return string 30804a78b87SAndreas Gohr */ 30904a78b87SAndreas Gohr public function getColor() 31004a78b87SAndreas Gohr { 31104a78b87SAndreas Gohr return '#999'; 31204a78b87SAndreas Gohr } 31304a78b87SAndreas Gohr 31404a78b87SAndreas Gohr /** 31504a78b87SAndreas Gohr * Return the SVG of the logo for this service 31604a78b87SAndreas Gohr * 31704a78b87SAndreas Gohr * Defaults to a logo.svg in the plugin directory 31804a78b87SAndreas Gohr * 31904a78b87SAndreas Gohr * @return string 32004a78b87SAndreas Gohr */ 32104a78b87SAndreas Gohr public function getSvgLogo() 32204a78b87SAndreas Gohr { 32304a78b87SAndreas Gohr $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg'; 32404a78b87SAndreas Gohr if (file_exists($logo)) return inlineSVG($logo); 32504a78b87SAndreas Gohr return ''; 32604a78b87SAndreas Gohr } 32704a78b87SAndreas Gohr 32804a78b87SAndreas Gohr /** 32904a78b87SAndreas Gohr * The oauth key 33004a78b87SAndreas Gohr * 33104a78b87SAndreas Gohr * @return string 33204a78b87SAndreas Gohr */ 33304a78b87SAndreas Gohr public function getKey() 33404a78b87SAndreas Gohr { 33504a78b87SAndreas Gohr return $this->getConf('key'); 33604a78b87SAndreas Gohr } 33704a78b87SAndreas Gohr 33804a78b87SAndreas Gohr /** 33904a78b87SAndreas Gohr * The oauth secret 34004a78b87SAndreas Gohr * 34104a78b87SAndreas Gohr * @return string 34204a78b87SAndreas Gohr */ 34304a78b87SAndreas Gohr public function getSecret() 34404a78b87SAndreas Gohr { 34504a78b87SAndreas Gohr return $this->getConf('secret'); 34604a78b87SAndreas Gohr } 34704a78b87SAndreas Gohr 34804a78b87SAndreas Gohr // endregion 34904a78b87SAndreas Gohr} 350