register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister'); } /** * Auto register this plugin with the oAuth authentication plugin */ public function handleRegister(Event $event, $param) { $event->data[$this->getServiceID()] = $this; } /** * Initialize the oAuth service * * @param string $storageId user based storage key (if available, yet) * @throws \OAuth\Common\Exception\Exception */ public function initOAuthService($storageId = '') { /** @var \helper_plugin_oauth $hlp */ $hlp = plugin_load('helper', 'oauth'); $credentials = new Credentials( $this->getKey(), $this->getSecret(), $hlp->redirectURI() ); $serviceFactory = new ServiceFactory(); $serviceFactory->setHttpClient(new HTTPClient()); $servicename = $this->getServiceID(); $serviceclass = $this->registerServiceClass(); if ($serviceclass) { $serviceFactory->registerService($servicename, $serviceclass); } if ($storageId) { $storage = new Storage($storageId); } else { $storage = new SessionStorage(); } $this->oAuth = $serviceFactory->createService( $servicename, $credentials, $storage, $this->getScopes() ); if ($this->oAuth === null) { throw new Exception('Failed to initialize Service ' . $this->getLabel()); } } /** * @return Abstract2Service|Abstract1Service * @throws Exception */ public function getOAuthService() { if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized'); return $this->oAuth; } /** * Once a user has been authenticated, the current token storage needs to be made permanent * * @param string $storageId * @throws Exception * @throws TokenNotFoundException */ public function upgradeStorage($storageId) { $oauth = $this->getOAuthService(); $service = $oauth->service(); $oldStorage = $oauth->getStorage(); $newStorage = new Storage($storageId); if ($oldStorage->hasAccessToken($service)) { $newStorage->storeAccessToken($service, $oldStorage->retrieveAccessToken($service)); } if ($oldStorage->hasAuthorizationState($service)) { $newStorage->storeAuthorizationState($service, $oldStorage->retrieveAuthorizationState($service)); } // fixme invalidate current oauth object? reinitialize it? } /** * Refresh a possibly outdated access token * * Does nothing when the current token is still good to use * * @return void * @throws MissingRefreshTokenException * @throws TokenNotFoundException * @throws TokenResponseException * @throws Exception */ public function refreshOutdatedToken() { $oauth = $this->getOAuthService(); if (!$oauth->getStorage()->hasAccessToken($oauth->service())) { // no token to refresh return; } $token = $oauth->getStorage()->retrieveAccessToken($oauth->service()); if ( $token->getEndOfLife() < 0 || $token->getEndOfLife() - time() > 3600 ) { // token is still good return; } $refreshToken = $token->getRefreshToken(); $token = $oauth->refreshAccessToken($token); // If the IDP did not provide a new refresh token, store the old one if (!$token->getRefreshToken()) { $token->setRefreshToken($refreshToken); $oauth->getStorage()->storeAccessToken($oauth->service(), $token); } } /** * Redirects to the service for requesting access * * This is the first step of oAuth authentication * * This implementation tries to abstract away differences between oAuth1 and oAuth2, * but might need to be overwritten for specific services * * @throws TokenResponseException * @throws \Exception */ public function login() { $oauth = $this->getOAuthService(); // store Farmer animal in oAuth state parameter /** @var \helper_plugin_farmer $farmer */ $farmer = plugin_load('helper', 'farmer'); $parameters = []; if ($farmer && $animal = $farmer->getAnimal()) { $parameters['state'] = urlencode(base64_encode(json_encode( [ 'animal' => $animal, 'state' => md5(random_int(0, mt_getrandmax())), ] ))); $oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']); } if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */ // extra request needed for oauth1 to request a request token $token = $oauth->requestRequestToken(); $parameters['oauth_token'] = $token->getRequestToken(); } $url = $oauth->getAuthorizationUri($parameters); send_redirect($url); } /** * Request access token * * This is the second step of oAuth authentication * * This implementation tries to abstract away differences between oAuth1 and oAuth2, * but might need to be overwritten for specific services * * Thrown exceptions indicate a non-successful login because of some error, appropriate messages * should be shown to the user. A return of false with no exceptions indicates that there was no * oauth data at all. This can probably be silently ignored. * * @return bool true if authentication was successful * @throws \OAuth\Common\Exception\Exception * @throws InvalidAuthorizationStateException */ public function checkToken() { global $INPUT; $oauth = $this->getOAuthService(); if (is_a($oauth, Abstract2Service::class)) { if (!$INPUT->get->has('code')) return false; $state = $INPUT->get->str('state', null); $accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state); } else { if (!$INPUT->get->has('oauth_token')) return false; /** @var TokenInterface $token */ $token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID()); $accessToken = $oauth->requestAccessToken( $INPUT->get->str('oauth_token'), $INPUT->get->str('oauth_verifier'), $token->getRequestTokenSecret() ); } if ( $accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES && !$accessToken->getRefreshToken() ) { msg('Service did not provide a Refresh Token. You will be logged out when the session expires.'); } return true; } /** * Return the Service Login Button * * @return string */ public function loginButton() { global $ID; $attr = buildAttributes([ 'href' => wl($ID, ['oauthlogin' => $this->getServiceID()], false, '&'), 'class' => 'plugin_oauth_' . $this->getServiceID(), 'style' => 'background-color: ' . $this->getColor(), ]); return '' . $this->getSvgLogo() . '' . $this->getLabel() . ' '; } // endregion // region overridable methods /** * Called on logout * * If there are required procedures for the service, you can implement them by overriding this. * * @return void */ public function logout() { } /** * Retrieve the user's data via API * * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps' * * Use the request() method of the oauth object to talk to the API * * @return array * @throws Exception * @see getOAuthService() */ abstract public function getUser(); /** * Return the scopes to request * * This should return the minimal scopes needed for accessing the user's data * * @return string[] */ public function getScopes() { return []; } /** * Return the user friendly name of the service * * Defaults to ServiceID. You may want to override this. * * @return string */ public function getLabel() { return ucfirst($this->getServiceID()); } /** * Return the internal name of the Service * * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in * the appropriate lusitantian oauth Service namespace * * @return string */ public function getServiceID() { $name = $this->getPluginName(); if (substr($name, 0, 5) === 'oauth') { $name = substr($name, 5); } return $name; } /** * Register a new Service * * @return string A fully qualified class name to register as new Service for your ServiceID */ public function registerServiceClass() { return null; } /** * Return the button color to use * * @return string */ public function getColor() { return '#999'; } /** * Return the SVG of the logo for this service * * Defaults to a logo.svg in the plugin directory * * @return string */ public function getSvgLogo() { $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg'; if (file_exists($logo)) return inlineSVG($logo); return ''; } /** * The oauth key * * @return string */ public function getKey() { return $this->getConf('key'); } /** * The oauth secret * * @return string */ public function getSecret() { return $this->getConf('secret'); } // endregion }