1<?php 2 3namespace OAuth\OAuth2\Service; 4 5use OAuth\Common\Consumer\CredentialsInterface; 6use OAuth\Common\Exception\Exception; 7use OAuth\Common\Service\AbstractService as BaseAbstractService; 8use OAuth\Common\Storage\TokenStorageInterface; 9use OAuth\Common\Http\Exception\TokenResponseException; 10use OAuth\Common\Http\Client\ClientInterface; 11use OAuth\Common\Http\Uri\UriInterface; 12use OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException; 13use OAuth\OAuth2\Service\Exception\InvalidScopeException; 14use OAuth\OAuth2\Service\Exception\MissingRefreshTokenException; 15use OAuth\Common\Token\TokenInterface; 16use OAuth\Common\Token\Exception\ExpiredTokenException; 17 18abstract class AbstractService extends BaseAbstractService implements ServiceInterface 19{ 20 /** @const OAUTH_VERSION */ 21 const OAUTH_VERSION = 2; 22 23 /** @var array */ 24 protected $scopes; 25 26 /** @var UriInterface|null */ 27 protected $baseApiUri; 28 29 /** @var bool */ 30 protected $stateParameterInAuthUrl; 31 32 /** 33 * @param CredentialsInterface $credentials 34 * @param ClientInterface $httpClient 35 * @param TokenStorageInterface $storage 36 * @param array $scopes 37 * @param UriInterface|null $baseApiUri 38 * @param bool $stateParameterInAutUrl 39 * 40 * @throws InvalidScopeException 41 */ 42 public function __construct( 43 CredentialsInterface $credentials, 44 ClientInterface $httpClient, 45 TokenStorageInterface $storage, 46 $scopes = array(), 47 UriInterface $baseApiUri = null, 48 $stateParameterInAutUrl = false 49 ) { 50 parent::__construct($credentials, $httpClient, $storage); 51 $this->stateParameterInAuthUrl = $stateParameterInAutUrl; 52 53 foreach ($scopes as $scope) { 54 if (!$this->isValidScope($scope)) { 55 throw new InvalidScopeException('Scope ' . $scope . ' is not valid for service ' . get_class($this)); 56 } 57 } 58 59 $this->scopes = $scopes; 60 61 $this->baseApiUri = $baseApiUri; 62 } 63 64 /** 65 * {@inheritdoc} 66 */ 67 public function getAuthorizationUri(array $additionalParameters = array()) 68 { 69 $parameters = array_merge( 70 $additionalParameters, 71 array( 72 'type' => 'web_server', 73 'client_id' => $this->credentials->getConsumerId(), 74 'redirect_uri' => $this->credentials->getCallbackUrl(), 75 'response_type' => 'code', 76 ) 77 ); 78 79 $parameters['scope'] = implode(' ', $this->scopes); 80 81 if ($this->needsStateParameterInAuthUrl()) { 82 if (!isset($parameters['state'])) { 83 $parameters['state'] = $this->generateAuthorizationState(); 84 } 85 $this->storeAuthorizationState($parameters['state']); 86 } 87 88 // Build the url 89 $url = clone $this->getAuthorizationEndpoint(); 90 foreach ($parameters as $key => $val) { 91 $url->addToQuery($key, $val); 92 } 93 94 return $url; 95 } 96 97 /** 98 * {@inheritdoc} 99 */ 100 public function requestAccessToken($code, $state = null) 101 { 102 if (null !== $state) { 103 $this->validateAuthorizationState($state); 104 } 105 106 $bodyParams = array( 107 'code' => $code, 108 'client_id' => $this->credentials->getConsumerId(), 109 'client_secret' => $this->credentials->getConsumerSecret(), 110 'redirect_uri' => $this->credentials->getCallbackUrl(), 111 'grant_type' => 'authorization_code', 112 ); 113 114 $responseBody = $this->httpClient->retrieveResponse( 115 $this->getAccessTokenEndpoint(), 116 $bodyParams, 117 $this->getExtraOAuthHeaders() 118 ); 119 120 $token = $this->parseAccessTokenResponse($responseBody); 121 $this->storage->storeAccessToken($this->service(), $token); 122 123 return $token; 124 } 125 126 /** 127 * Sends an authenticated API request to the path provided. 128 * If the path provided is not an absolute URI, the base API Uri (must be passed into constructor) will be used. 129 * 130 * @param string|UriInterface $path 131 * @param string $method HTTP method 132 * @param array $body Request body if applicable. 133 * @param array $extraHeaders Extra headers if applicable. These will override service-specific 134 * any defaults. 135 * 136 * @return string 137 * 138 * @throws ExpiredTokenException 139 * @throws Exception 140 */ 141 public function request($path, $method = 'GET', $body = null, array $extraHeaders = array()) 142 { 143 $uri = $this->determineRequestUriFromPath($path, $this->baseApiUri); 144 $token = $this->storage->retrieveAccessToken($this->service()); 145 146 if ($token->getEndOfLife() !== TokenInterface::EOL_NEVER_EXPIRES 147 && $token->getEndOfLife() !== TokenInterface::EOL_UNKNOWN 148 && time() > $token->getEndOfLife() 149 ) { 150 throw new ExpiredTokenException( 151 sprintf( 152 'Token expired on %s at %s', 153 date('m/d/Y', $token->getEndOfLife()), 154 date('h:i:s A', $token->getEndOfLife()) 155 ) 156 ); 157 } 158 159 // add the token where it may be needed 160 if (static::AUTHORIZATION_METHOD_HEADER_OAUTH === $this->getAuthorizationMethod()) { 161 $extraHeaders = array_merge(array('Authorization' => 'OAuth ' . $token->getAccessToken()), $extraHeaders); 162 } elseif (static::AUTHORIZATION_METHOD_QUERY_STRING === $this->getAuthorizationMethod()) { 163 $uri->addToQuery('access_token', $token->getAccessToken()); 164 } elseif (static::AUTHORIZATION_METHOD_QUERY_STRING_V2 === $this->getAuthorizationMethod()) { 165 $uri->addToQuery('oauth2_access_token', $token->getAccessToken()); 166 } elseif (static::AUTHORIZATION_METHOD_QUERY_STRING_V3 === $this->getAuthorizationMethod()) { 167 $uri->addToQuery('apikey', $token->getAccessToken()); 168 } elseif (static::AUTHORIZATION_METHOD_HEADER_BEARER === $this->getAuthorizationMethod()) { 169 $extraHeaders = array_merge(array('Authorization' => 'Bearer ' . $token->getAccessToken()), $extraHeaders); 170 } 171 172 $extraHeaders = array_merge($this->getExtraApiHeaders(), $extraHeaders); 173 174 return $this->httpClient->retrieveResponse($uri, $body, $extraHeaders, $method); 175 } 176 177 /** 178 * Accessor to the storage adapter to be able to retrieve tokens 179 * 180 * @return TokenStorageInterface 181 */ 182 public function getStorage() 183 { 184 return $this->storage; 185 } 186 187 /** 188 * Refreshes an OAuth2 access token. 189 * 190 * @param TokenInterface $token 191 * 192 * @return TokenInterface $token 193 * 194 * @throws MissingRefreshTokenException 195 */ 196 public function refreshAccessToken(TokenInterface $token) 197 { 198 $refreshToken = $token->getRefreshToken(); 199 200 if (empty($refreshToken)) { 201 throw new MissingRefreshTokenException(); 202 } 203 204 $parameters = array( 205 'grant_type' => 'refresh_token', 206 'type' => 'web_server', 207 'client_id' => $this->credentials->getConsumerId(), 208 'client_secret' => $this->credentials->getConsumerSecret(), 209 'refresh_token' => $refreshToken, 210 ); 211 212 $responseBody = $this->httpClient->retrieveResponse( 213 $this->getAccessTokenEndpoint(), 214 $parameters, 215 $this->getExtraOAuthHeaders() 216 ); 217 $token = $this->parseAccessTokenResponse($responseBody); 218 $this->storage->storeAccessToken($this->service(), $token); 219 220 return $token; 221 } 222 223 /** 224 * Return whether or not the passed scope value is valid. 225 * 226 * @param string $scope 227 * 228 * @return bool 229 */ 230 public function isValidScope($scope) 231 { 232 $reflectionClass = new \ReflectionClass(get_class($this)); 233 234 return in_array($scope, $reflectionClass->getConstants(), true); 235 } 236 237 /** 238 * Check if the given service need to generate a unique state token to build the authorization url 239 * 240 * @return bool 241 */ 242 public function needsStateParameterInAuthUrl() 243 { 244 return $this->stateParameterInAuthUrl; 245 } 246 247 /** 248 * Validates the authorization state against a given one 249 * 250 * @param string $state 251 * @throws InvalidAuthorizationStateException 252 */ 253 protected function validateAuthorizationState($state) 254 { 255 if ($this->retrieveAuthorizationState() !== $state) { 256 throw new InvalidAuthorizationStateException(); 257 } 258 } 259 260 /** 261 * Generates a random string to be used as state 262 * 263 * @return string 264 */ 265 protected function generateAuthorizationState() 266 { 267 return md5(rand()); 268 } 269 270 /** 271 * Retrieves the authorization state for the current service 272 * 273 * @return string 274 */ 275 protected function retrieveAuthorizationState() 276 { 277 return $this->storage->retrieveAuthorizationState($this->service()); 278 } 279 280 /** 281 * Stores a given authorization state into the storage 282 * 283 * @param string $state 284 */ 285 protected function storeAuthorizationState($state) 286 { 287 $this->storage->storeAuthorizationState($this->service(), $state); 288 } 289 290 /** 291 * Return any additional headers always needed for this service implementation's OAuth calls. 292 * 293 * @return array 294 */ 295 protected function getExtraOAuthHeaders() 296 { 297 return array(); 298 } 299 300 /** 301 * Return any additional headers always needed for this service implementation's API calls. 302 * 303 * @return array 304 */ 305 protected function getExtraApiHeaders() 306 { 307 return array(); 308 } 309 310 /** 311 * Parses the access token response and returns a TokenInterface. 312 * 313 * @abstract 314 * 315 * @param string $responseBody 316 * 317 * @return TokenInterface 318 * 319 * @throws TokenResponseException 320 */ 321 abstract protected function parseAccessTokenResponse($responseBody); 322 323 /** 324 * Returns a class constant from ServiceInterface defining the authorization method used for the API 325 * Header is the sane default. 326 * 327 * @return int 328 */ 329 protected function getAuthorizationMethod() 330 { 331 return static::AUTHORIZATION_METHOD_HEADER_OAUTH; 332 } 333} 334