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 $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 $oauth->requestAccessToken( 164 $INPUT->get->str('oauth_token'), 165 $INPUT->get->str('oauth_verifier'), 166 $token->getRequestTokenSecret() 167 ); 168 } 169 return true; 170 } 171 172 /** 173 * Return the Service Login Button 174 * 175 * @return string 176 */ 177 public function loginButton() 178 { 179 global $ID; 180 181 $attr = buildAttributes([ 182 'href' => wl($ID, array('oauthlogin' => $this->getServiceID()), false, '&'), 183 'class' => 'plugin_oauth_' . $this->getServiceID(), 184 'style' => 'background-color: ' . $this->getColor(), 185 ]); 186 187 return '<a ' . $attr . '>' . $this->getSvgLogo() . '<span>' . $this->getLabel() . '</span></a> '; 188 } 189 // endregion 190 191 // region overridable methods 192 193 /** 194 * Retrieve the user's data via API 195 * 196 * The returned array needs to contain at least 'email', 'name', 'user' and optionally 'grps' 197 * 198 * Use the request() method of the oauth object to talk to the API 199 * 200 * @return array 201 * @throws Exception 202 * @see getOAuthService() 203 */ 204 abstract public function getUser(); 205 206 /** 207 * Return the scopes to request 208 * 209 * This should return the minimal scopes needed for accessing the user's data 210 * 211 * @return string[] 212 */ 213 public function getScopes() 214 { 215 return []; 216 } 217 218 /** 219 * Return the user friendly name of the service 220 * 221 * Defaults to ServiceID. You may want to override this. 222 * 223 * @return string 224 */ 225 public function getLabel() 226 { 227 return ucfirst($this->getServiceID()); 228 } 229 230 /** 231 * Return the internal name of the Service 232 * 233 * Defaults to the plugin name (without oauth prefix). This has to match the Service class name in 234 * the appropriate lusitantian oauth Service namespace 235 * 236 * @return string 237 */ 238 public function getServiceID() 239 { 240 $name = $this->getPluginName(); 241 if (substr($name, 0, 5) === 'oauth') { 242 $name = substr($name, 5); 243 } 244 245 return $name; 246 } 247 248 /** 249 * Register a new Service 250 * 251 * @return string A fully qualified class name to register as new Service for your ServiceID 252 */ 253 public function registerServiceClass() 254 { 255 return null; 256 } 257 258 /** 259 * Return the button color to use 260 * 261 * @return string 262 */ 263 public function getColor() 264 { 265 return '#999'; 266 } 267 268 /** 269 * Return the SVG of the logo for this service 270 * 271 * Defaults to a logo.svg in the plugin directory 272 * 273 * @return string 274 */ 275 public function getSvgLogo() 276 { 277 $logo = DOKU_PLUGIN . $this->getPluginName() . '/logo.svg'; 278 if (file_exists($logo)) return inlineSVG($logo); 279 return ''; 280 } 281 282 /** 283 * The oauth key 284 * 285 * @return string 286 */ 287 public function getKey() 288 { 289 return $this->getConf('key'); 290 } 291 292 /** 293 * The oauth secret 294 * 295 * @return string 296 */ 297 public function getSecret() 298 { 299 return $this->getConf('secret'); 300 } 301 302 // endregion 303} 304