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