1<?php 2 3namespace dokuwiki\plugin\oauth; 4 5use dokuwiki\Extension\ActionPlugin; 6use OAuth\Common\Consumer\Credentials; 7use OAuth\Common\Http\Exception\TokenResponseException; 8use OAuth\OAuth1\Service\AbstractService as Abstract1Service; 9use OAuth\OAuth1\Token\TokenInterface; 10use OAuth\OAuth2\Service\AbstractService as Abstract2Service; 11use OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException; 12use OAuth\ServiceFactory; 13 14/** 15 * Base class to implement a Backend Service for the oAuth Plugin 16 */ 17abstract class Adapter extends ActionPlugin 18{ 19 /** 20 * @var Abstract2Service|Abstract1Service 21 * @see getOAuthService() use this to ensure it's intialized 22 */ 23 protected $oAuth; 24 25 // region internal methods 26 27 /** 28 * Auto register this plugin with the oAuth authentication plugin 29 * 30 * @inheritDoc 31 */ 32 public function register(\Doku_Event_Handler $controller) 33 { 34 $controller->register_hook('PLUGIN_OAUTH_BACKEND_REGISTER', 'AFTER', $this, 'handleRegister'); 35 } 36 37 /** 38 * Auto register this plugin with the oAuth authentication plugin 39 */ 40 public function handleRegister(\Doku_Event $event, $param) 41 { 42 $event->data[$this->getServiceID()] = $this; 43 } 44 45 /** 46 * Initialize the oAuth service 47 * 48 * @param string $guid UIID for the user to authenticate 49 * @throws \OAuth\Common\Exception\Exception 50 */ 51 public function initOAuthService($guid) 52 { 53 /** @var \helper_plugin_oauth $hlp */ 54 $hlp = plugin_load('helper', 'oauth'); 55 56 $credentials = new Credentials( 57 $this->getKey(), 58 $this->getSecret(), 59 $hlp->redirectURI() 60 ); 61 62 $serviceFactory = new ServiceFactory(); 63 $serviceFactory->setHttpClient(new HTTPClient()); 64 $servicename = $this->getServiceID(); 65 $serviceclass = $this->registerServiceClass(); 66 if ($serviceclass) { 67 $serviceFactory->registerService($servicename, $serviceclass); 68 } 69 70 $this->oAuth = $serviceFactory->createService( 71 $servicename, 72 $credentials, 73 new Storage($guid), 74 $this->getScopes() 75 ); 76 77 if ($this->oAuth === null) { 78 throw new Exception('Failed to initialize Service ' . $this->getLabel()); 79 } 80 } 81 82 /** 83 * @return Abstract2Service|Abstract1Service 84 * @throws Exception 85 */ 86 public function getOAuthService() 87 { 88 if ($this->oAuth === null) throw new Exception('OAuth Service not properly initialized'); 89 return $this->oAuth; 90 } 91 92 /** 93 * Redirects to the service for requesting access 94 * 95 * This is the first step of oAuth authentication 96 * 97 * This implementation tries to abstract away differences between oAuth1 and oAuth2, 98 * but might need to be overwritten for specific services 99 * 100 * @throws TokenResponseException 101 * @throws Exception 102 */ 103 public function login() 104 { 105 $oauth = $this->getOAuthService(); 106 107 // store Farmer animal in oAuth state parameter 108 /** @var \helper_plugin_farmer $farmer */ 109 $farmer = plugin_load('helper', 'farmer'); 110 $parameters = []; 111 if ($farmer && $animal = $farmer->getAnimal()) { 112 $parameters['state'] = urlencode(base64_encode(json_encode( 113 [ 114 'animal' => $animal, 115 'state' => md5(rand()), 116 ] 117 ))); 118 $oauth->getStorage()->storeAuthorizationState($oauth->service(), $parameters['state']); 119 } 120 121 if (is_a($oauth, Abstract1Service::class)) { /* oAuth1 handling */ 122 // extra request needed for oauth1 to request a request token 123 $token = $oauth->requestRequestToken(); 124 $parameters['oauth_token'] = $token->getRequestToken(); 125 } 126 $url = $oauth->getAuthorizationUri($parameters); 127 128 send_redirect($url); 129 } 130 131 /** 132 * Request access token 133 * 134 * This is the second step of oAuth authentication 135 * 136 * This implementation tries to abstract away differences between oAuth1 and oAuth2, 137 * but might need to be overwritten for specific services 138 * 139 * Thrown exceptions indicate a non-successful login because of some error, appropriate messages 140 * should be shown to the user. A return of false with no exceptions indicates that there was no 141 * oauth data at all. This can probably be silently ignored. 142 * 143 * @return bool true if authentication was successful 144 * @throws \OAuth\Common\Exception\Exception 145 * @throws InvalidAuthorizationStateException 146 */ 147 public function checkToken() 148 { 149 global $INPUT; 150 151 $oauth = $this->getOAuthService(); 152 153 if (is_a($oauth, Abstract2Service::class)) { 154 /** @var Abstract2Service $oauth */ 155 if (!$INPUT->get->has('code')) return false; 156 $state = $INPUT->get->str('state', null); 157 $accessToken = $oauth->requestAccessToken($INPUT->get->str('code'), $state); 158 } else { 159 /** @var Abstract1Service $oauth */ 160 if (!$INPUT->get->has('oauth_token')) return false; 161 /** @var TokenInterface $token */ 162 $token = $oauth->getStorage()->retrieveAccessToken($this->getServiceID()); 163 $accessToken = $oauth->requestAccessToken( 164 $INPUT->get->str('oauth_token'), 165 $INPUT->get->str('oauth_verifier'), 166 $token->getRequestTokenSecret() 167 ); 168 } 169 170 if ( 171 $accessToken->getEndOfLife() !== $accessToken::EOL_NEVER_EXPIRES && 172 !$accessToken->getRefreshToken()) { 173 msg('Service did not provide a Refresh Token. You will be logged out when the session expires.'); 174 } 175 176 return true; 177 } 178 179 /** 180 * Return the Service Login Button 181 * 182 * @return string 183 */ 184 public function loginButton() 185 { 186 global $ID; 187 188 $attr = buildAttributes([ 189 'href' => wl($ID, array('oauthlogin' => $this->getServiceID()), false, '&'), 190 'class' => 'plugin_oauth_' . $this->getServiceID(), 191 'style' => 'background-color: ' . $this->getColor(), 192 ]); 193 194 return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> '; 195 } 196 // endregion 197 198 // region overridable methods 199 200 /** 201 * Retrieve the user's data via API 202 * 203 * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps' 204 * 205 * Use the request() method of the oauth object to talk to the API 206 * 207 * @return array 208 * @throws Exception 209 * @see getOAuthService() 210 */ 211 abstract public function getUser(); 212 213 /** 214 * Return the scopes to request 215 * 216 * This should return the minimal scopes needed for accessing the user's data 217 * 218 * @return string[] 219 */ 220 public function getScopes() 221 { 222 return []; 223 } 224 225 /** 226 * Return the user friendly name of the service 227 * 228 * Defaults to ServiceID. You may want to override this. 229 * 230 * @return string 231 */ 232 public function getLabel() 233 { 234 return ucfirst($this->getServiceID()); 235 } 236 237 /** 238 * Return the internal name of the Service 239 * 240 * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in 241 * the appropriate lusitantian oauth Service namespace 242 * 243 * @return string 244 */ 245 public function getServiceID() 246 { 247 $name = $this->getPluginName(); 248 if (substr($name, 0, 5) === 'oauth') { 249 $name = substr($name, 5); 250 } 251 252 return $name; 253 } 254 255 /** 256 * Register a new Service 257 * 258 * @return string A fully qualified class name to register as new Service for your ServiceID 259 */ 260 public function registerServiceClass() 261 { 262 return null; 263 } 264 265 /** 266 * Return the button color to use 267 * 268 * @return string 269 */ 270 public function getColor() 271 { 272 return '#999'; 273 } 274 275 /** 276 * Return the SVG of the logo for this service 277 * 278 * Defaults to a logo.svg in the plugin directory 279 * 280 * @return string 281 */ 282 public function getSvgLogo() 283 { 284 $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg'; 285 if (file_exists($logo)) return inlineSVG($logo); 286 return ''; 287 } 288 289 /** 290 * The oauth key 291 * 292 * @return string 293 */ 294 public function getKey() 295 { 296 return $this->getConf('key'); 297 } 298 299 /** 300 * The oauth secret 301 * 302 * @return string 303 */ 304 public function getSecret() 305 { 306 return $this->getConf('secret'); 307 } 308 309 // endregion 310} 311