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