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