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