Completed
Pull Request — master (#569)
by
unknown
01:37
created

AbstractService::carryRefreshToken()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
3
namespace OAuth\OAuth2\Service;
4
5
use OAuth\Common\Consumer\CredentialsInterface;
6
use OAuth\Common\Http\Client\ClientInterface;
7
use OAuth\Common\Http\Uri\UriInterface;
8
use OAuth\Common\Service\AbstractService as BaseAbstractService;
9
use OAuth\Common\Storage\TokenStorageInterface;
10
use OAuth\Common\Token\Exception\ExpiredTokenException;
11
use OAuth\Common\Token\TokenInterface;
12
use OAuth\OAuth2\Service\Exception\InvalidAuthorizationStateException;
13
use OAuth\OAuth2\Service\Exception\InvalidScopeException;
14
use OAuth\OAuth2\Service\Exception\MissingRefreshTokenException;
15
use ReflectionClass;
16
17
abstract class AbstractService extends BaseAbstractService implements ServiceInterface
18
{
19
    /** @const OAUTH_VERSION */
20
    const OAUTH_VERSION = 2;
21
22
    /** @var array */
23
    protected $scopes;
24
25
    /** @var null|UriInterface */
26
    protected $baseApiUri;
27
28
    /** @var bool */
29
    protected $stateParameterInAuthUrl;
30
31
    /** @var string */
32
    protected $apiVersion;
33
34
    /**
35
     * @param array                 $scopes
36
     * @param bool $stateParameterInAutUrl
37
     * @param string                $apiVersion
38
     */
39
    public function __construct(
40
        CredentialsInterface $credentials,
41
        ClientInterface $httpClient,
42
        TokenStorageInterface $storage,
43
        $scopes = [],
44
        ?UriInterface $baseApiUri = null,
45
        $stateParameterInAutUrl = false,
46
        $apiVersion = ''
47
    ) {
48
        parent::__construct($credentials, $httpClient, $storage);
49
        $this->stateParameterInAuthUrl = $stateParameterInAutUrl;
50
51
        foreach ($scopes as $scope) {
52
            if (!$this->isValidScope($scope)) {
53
                throw new InvalidScopeException('Scope ' . $scope . ' is not valid for service ' . get_class($this));
54
            }
55
        }
56
57
        $this->scopes = $scopes;
58
59
        $this->baseApiUri = $baseApiUri;
60
61
        $this->apiVersion = $apiVersion;
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function getAuthorizationUri(array $additionalParameters = [])
68
    {
69
        $parameters = array_merge(
70
            $additionalParameters,
71
            [
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->getScopesDelimiter(), $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 = [
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
    public function request($path, $method = 'GET', $body = null, array $extraHeaders = [])
139
    {
140
        $uri = $this->determineRequestUriFromPath($path, $this->baseApiUri);
141
        $token = $this->storage->retrieveAccessToken($this->service());
142
143
        if ($token->getEndOfLife() !== TokenInterface::EOL_NEVER_EXPIRES
144
            && $token->getEndOfLife() !== TokenInterface::EOL_UNKNOWN
145
            && time() > $token->getEndOfLife()
146
        ) {
147
            throw new ExpiredTokenException(
148
                sprintf(
149
                    'Token expired on %s at %s',
150
                    date('m/d/Y', $token->getEndOfLife()),
151
                    date('h:i:s A', $token->getEndOfLife())
152
                )
153
            );
154
        }
155
156
        // add the token where it may be needed
157
        if (static::AUTHORIZATION_METHOD_HEADER_OAUTH === $this->getAuthorizationMethod()) {
158
            $extraHeaders = array_merge(['Authorization' => 'OAuth ' . $token->getAccessToken()], $extraHeaders);
159
        } elseif (static::AUTHORIZATION_METHOD_QUERY_STRING === $this->getAuthorizationMethod()) {
160
            $uri->addToQuery('access_token', $token->getAccessToken());
161
        } elseif (static::AUTHORIZATION_METHOD_QUERY_STRING_V2 === $this->getAuthorizationMethod()) {
162
            $uri->addToQuery('oauth2_access_token', $token->getAccessToken());
163
        } elseif (static::AUTHORIZATION_METHOD_QUERY_STRING_V3 === $this->getAuthorizationMethod()) {
164
            $uri->addToQuery('apikey', $token->getAccessToken());
165
        } elseif (static::AUTHORIZATION_METHOD_QUERY_STRING_V4 === $this->getAuthorizationMethod()) {
166
            $uri->addToQuery('auth', $token->getAccessToken());
167
        } elseif (static::AUTHORIZATION_METHOD_HEADER_BEARER === $this->getAuthorizationMethod()) {
168
            $extraHeaders = array_merge(['Authorization' => 'Bearer ' . $token->getAccessToken()], $extraHeaders);
169
        } elseif (static::AUTHORIZATION_METHOD_HEADER_TOKEN === $this->getAuthorizationMethod()) {
170
            $extraHeaders = array_merge(array('Authorization' => 'token ' . $token->getAccessToken()), $extraHeaders);
171
        }
172
173
        $extraHeaders = array_merge($this->getExtraApiHeaders(), $extraHeaders);
174
175
        return $this->httpClient->retrieveResponse($uri, $body, $extraHeaders, $method);
176
    }
177
178
    /**
179
     * Accessor to the storage adapter to be able to retrieve tokens.
180
     *
181
     * @return TokenStorageInterface
182
     */
183
    public function getStorage()
184
    {
185
        return $this->storage;
186
    }
187
188
    /**
189
     * Refreshes an OAuth2 access token.
190
     *
191
     * @return TokenInterface $token
192
     */
193
    public function refreshAccessToken(TokenInterface $token)
194
    {
195
        $refreshToken = $token->getRefreshToken();
196
197
        if (empty($refreshToken)) {
198
            throw new MissingRefreshTokenException();
199
        }
200
201
        $parameters = [
202
            'grant_type' => 'refresh_token',
203
            'type' => 'web_server',
204
            'client_id' => $this->credentials->getConsumerId(),
205
            'client_secret' => $this->credentials->getConsumerSecret(),
206
            'refresh_token' => $refreshToken,
207
        ];
208
209
        $responseBody = $this->httpClient->retrieveResponse(
210
            $this->getAccessTokenEndpoint(),
211
            $parameters,
212
            $this->getExtraOAuthHeaders()
213
        );
214
215
        $responseBody = $this->carryRefreshToken($responseBody, $refreshToken);
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
     * Carry the refresh_token for next time getting refreshed
249
     *
250
     * @param $responseBody
251
     *
252
     * @param $refreshToken
253
     *
254
     * @return string
255
     */
256
    protected function carryRefreshToken($responseBody, $refreshToken)
257
    {
258
        $responseJson = json_decode($responseBody, true);
259
        if (empty($responseJson['refresh_token'])) {
260
            $responseJson['refresh_token'] = $refreshToken;
261
            return json_encode($responseJson);
262
        } else {
263
            return $responseBody;
264
        }
265
    }
266
267
    /**
268
     * Validates the authorization state against a given one.
269
     *
270
     * @param string $state
271
     */
272
    protected function validateAuthorizationState($state): void
273
    {
274
        if ($this->retrieveAuthorizationState() !== $state) {
275
            throw new InvalidAuthorizationStateException();
276
        }
277
    }
278
279
    /**
280
     * Generates a random string to be used as state.
281
     *
282
     * @return string
283
     */
284
    protected function generateAuthorizationState()
285
    {
286
        return md5(mt_rand());
287
    }
288
289
    /**
290
     * Retrieves the authorization state for the current service.
291
     *
292
     * @return string
293
     */
294
    protected function retrieveAuthorizationState()
295
    {
296
        return $this->storage->retrieveAuthorizationState($this->service());
297
    }
298
299
    /**
300
     * Stores a given authorization state into the storage.
301
     *
302
     * @param string $state
303
     */
304
    protected function storeAuthorizationState($state): void
305
    {
306
        $this->storage->storeAuthorizationState($this->service(), $state);
307
    }
308
309
    /**
310
     * Return any additional headers always needed for this service implementation's OAuth calls.
311
     *
312
     * @return array
313
     */
314
    protected function getExtraOAuthHeaders()
315
    {
316
        return [];
317
    }
318
319
    /**
320
     * Return any additional headers always needed for this service implementation's API calls.
321
     *
322
     * @return array
323
     */
324
    protected function getExtraApiHeaders()
325
    {
326
        return [];
327
    }
328
329
    /**
330
     * Parses the access token response and returns a TokenInterface.
331
     *
332
     * @abstract
333
     *
334
     * @param string $responseBody
335
     *
336
     * @return TokenInterface
337
     */
338
    abstract protected function parseAccessTokenResponse($responseBody);
339
340
    /**
341
     * Returns a class constant from ServiceInterface defining the authorization method used for the API
342
     * Header is the sane default.
343
     *
344
     * @return int
345
     */
346
    protected function getAuthorizationMethod()
347
    {
348
        return static::AUTHORIZATION_METHOD_HEADER_OAUTH;
349
    }
350
351
    /**
352
     * Returns api version string if is set else retrun empty string.
353
     *
354
     * @return string
355
     */
356
    protected function getApiVersionString()
357
    {
358
        return !(empty($this->apiVersion)) ? '/' . $this->apiVersion : '';
359
    }
360
361
    /**
362
     * Returns delimiter to scopes in getAuthorizationUri
363
     * For services that do not fully respect the Oauth's RFC,
364
     * and use scopes with commas as delimiter.
365
     *
366
     * @return string
367
     */
368
    protected function getScopesDelimiter()
369
    {
370
        return ' ';
371
    }
372
}
373