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