Passed
Push — master ( 65f9c5...2902c1 )
by Aleksei
07:53
created

OAuth1::setRequestTokenMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\AuthClient;
6
7
use InvalidArgumentException;
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use Yiisoft\Json\Json;
11
12
/**
13
 * OAuth1 serves as a client for the OAuth 1/1.0a flow.
14
 *
15
 * In order to acquire access token perform following sequence:
16
 *
17
 * ```php
18
 * use Yiisoft\Yii\AuthClient\OAuth1;
19
 *
20
 * // assuming class MyAuthClient extends OAuth1
21
 * $oauthClient = new MyAuthClient();
22
 * $requestToken = $oauthClient->fetchRequestToken(); // Get request token
23
 * $url = $oauthClient->buildAuthUrl($requestToken); // Get authorization URL
24
 * return Yii::getApp()->getResponse()->redirect($url); // Redirect to authorization URL
25
 *
26
 * // After user returns at our site:
27
 * $accessToken = $oauthClient->fetchAccessToken(Yii::getApp()->request->get('oauth_token'), $requestToken); // Upgrade to access token
28
 * ```
29
 *
30
 * @see https://oauth.net/1/
31
 * @see https://tools.ietf.org/html/rfc5849
32
 */
33
abstract class OAuth1 extends BaseOAuth
34
{
35
    private const PROTOCOL_VERSION = '1.0';
36
37
    /**
38
     * @var string OAuth consumer key.
39
     */
40
    protected string $consumerKey = '';
41
    /**
42
     * @var string OAuth consumer secret.
43
     */
44
    protected string $consumerSecret = '';
45
    /**
46
     * @var string OAuth request token URL.
47
     */
48
    protected string $requestTokenUrl;
49
    /**
50
     * @var string request token HTTP method.
51
     */
52
    protected string $requestTokenMethod = 'GET';
53
    /**
54
     * @var string OAuth access token URL.
55
     */
56
    protected string $accessTokenUrl;
57
    /**
58
     * @var string access token HTTP method.
59
     */
60
    protected string $accessTokenMethod = 'GET';
61
    /**
62
     * @var array|null list of the request methods, which require adding 'Authorization' header.
63
     * By default only POST requests will have 'Authorization' header.
64
     * You may set this option to `null` in order to make all requests to use 'Authorization' header.
65
     */
66
    protected ?array $authorizationHeaderMethods = ['POST'];
67
68
    /**
69
     * Fetches the OAuth request token.
70
     *
71
     * @param ServerRequestInterface $incomingRequest
72
     * @param array $params additional request params.
73
     *
74
     * @throws \Yiisoft\Factory\Exceptions\InvalidConfigException
75
     *
76
     * @return OAuthToken request token.
77
     */
78
    public function fetchRequestToken(ServerRequestInterface $incomingRequest, array $params = [])
79
    {
80
        $this->setAccessToken(null);
81
        $defaultParams = [
82
            'oauth_consumer_key' => $this->consumerKey,
83
            'oauth_callback' => $this->getReturnUrl($incomingRequest),
84
            //'xoauth_displayname' => Yii::getApp()->name,
85
        ];
86
        if (!empty($this->getScope())) {
87
            $defaultParams['scope'] = $this->getScope();
88
        }
89
90
        $request = $this->createRequest(
91
            $this->requestTokenMethod,
92
            $this->requestTokenUrl . '?' . http_build_query(
93
                array_merge($defaultParams, $params)
94
            )
95
        );
96
97
        $request = $this->signRequest($request);
98
        $response = $this->sendRequest($request);
99
100
        $token = $this->createToken(
101
            [
102
                'setParams()' => [Json::decode($response->getBody()->getContents())],
103
            ]
104
        );
105
        $this->setState('requestToken', $token);
106
107
        return $token;
108
    }
109
110
    /**
111
     * Sign given request with {@see signatureMethod}.
112
     *
113
     * @param RequestInterface $request request instance.
114
     * @param OAuthToken|null $token OAuth token to be used for signature, if not set {@see accessToken} will be used.
115
     *
116
     * @return RequestInterface
117
     */
118 2
    public function signRequest(RequestInterface $request, ?OAuthToken $token = null): RequestInterface
119
    {
120 2
        $params = RequestUtil::getParams($request);
121
122 2
        if (isset($params['oauth_signature_method']) || $request->hasHeader('authorization')) {
123
            // avoid double sign of request
124
            return $request;
125
        }
126
127 2
        if (empty($request->getUri()->getQuery())) {
128 1
            $params = $this->generateCommonRequestParams();
129
        } else {
130 1
            $params = array_merge($this->generateCommonRequestParams(), $params);
131
        }
132
133 2
        $url = $request->getUri()->__toString();
134
135 2
        $signatureMethod = $this->getSignatureMethod();
136
137 2
        $params['oauth_signature_method'] = $signatureMethod->getName();
138 2
        $signatureBaseString = $this->composeSignatureBaseString($request->getMethod(), $url, $params);
139 2
        $signatureKey = $this->composeSignatureKey($token);
140 2
        $params['oauth_signature'] = $signatureMethod->generateSignature($signatureBaseString, $signatureKey);
141
142
        if (
143 2
            $this->authorizationHeaderMethods === null || in_array(
144 2
                strtoupper($request->getMethod()),
145 2
                array_map(
146 2
                    'strtoupper',
147 2
                    $this->authorizationHeaderMethods
148
                ),
149 2
                true
150
            )
151
        ) {
152 1
            $authorizationHeader = $this->composeAuthorizationHeader($params);
153 1
            if (!empty($authorizationHeader)) {
154 1
                foreach ($authorizationHeader as $name => $value) {
155 1
                    $request = $request->withHeader($name, $value);
156
                }
157
158
                // removing authorization header params, avoiding duplicate param server error :
159 1
                foreach ($params as $key => $value) {
160 1
                    if (substr_compare($key, 'oauth', 0, 5) === 0) {
161 1
                        unset($params[$key]);
162
                    }
163
                }
164
            }
165
        }
166
167 2
        $uri = $request->getUri()->withQuery(http_build_query($params));
168 2
        return $request->withUri($uri);
169
    }
170
171
    /**
172
     * Generate common request params like version, timestamp etc.
173
     *
174
     * @return array common request params.
175
     */
176 2
    protected function generateCommonRequestParams(): array
177
    {
178
        return [
179 2
            'oauth_version' => self::PROTOCOL_VERSION,
180 2
            'oauth_nonce' => $this->generateNonce(),
181 2
            'oauth_timestamp' => $this->generateTimestamp(),
182
        ];
183
    }
184
185
    /**
186
     * Generates nonce value.
187
     *
188
     * @return string nonce value.
189
     */
190 2
    protected function generateNonce(): string
191
    {
192 2
        return md5(microtime() . mt_rand());
193
    }
194
195
    /**
196
     * Generates timestamp.
197
     *
198
     * @return int timestamp.
199
     */
200 2
    protected function generateTimestamp(): int
201
    {
202 2
        return time();
203
    }
204
205
    /**
206
     * Creates signature base string, which will be signed by {@see signatureMethod}.
207
     *
208
     * @param string $method request method.
209
     * @param string $url request URL.
210
     * @param array $params request params.
211
     *
212
     * @return string base signature string.
213
     */
214 2
    protected function composeSignatureBaseString($method, $url, array $params)
215
    {
216 2
        if (strpos($url, '?') !== false) {
217 1
            [$url, $queryString] = explode('?', $url, 2);
218 1
            parse_str($queryString, $urlParams);
219 1
            $params = array_merge($urlParams, $params);
220
        }
221 2
        unset($params['oauth_signature']);
222 2
        uksort(
223 2
            $params,
224 2
            'strcmp'
225
        ); // Parameters are sorted by name, using lexicographical byte value ordering. Ref: Spec: 9.1.1
226
        $parts = [
227 2
            strtoupper($method),
228 2
            $url,
229 2
            http_build_query($params, '', '&', PHP_QUERY_RFC3986),
230
        ];
231 2
        $parts = array_map('rawurlencode', $parts);
232
233 2
        return implode('&', $parts);
234
    }
235
236
    /**
237
     * Composes request signature key.
238
     *
239
     * @param OAuthToken|null $token OAuth token to be used for signature key.
240
     *
241
     * @return string signature key.
242
     */
243 2
    protected function composeSignatureKey($token = null): string
244
    {
245
        $signatureKeyParts = [
246 2
            $this->consumerSecret,
247
        ];
248
249 2
        if ($token === null) {
250 2
            $token = $this->getAccessToken();
251
        }
252 2
        if (is_object($token)) {
253
            $signatureKeyParts[] = $token->getTokenSecret();
254
        } else {
255 2
            $signatureKeyParts[] = '';
256
        }
257
258 2
        $signatureKeyParts = array_map('rawurlencode', $signatureKeyParts);
259
260 2
        return implode('&', $signatureKeyParts);
261
    }
262
263
    /**
264
     * Composes authorization header.
265
     *
266
     * @param array $params request params.
267
     * @param string $realm authorization realm.
268
     *
269
     * @return array authorization header in format: [name => content].
270
     */
271 4
    public function composeAuthorizationHeader(array $params, $realm = '')
272
    {
273 4
        $header = 'OAuth';
274 4
        $headerParams = [];
275 4
        if (!empty($realm)) {
276 1
            $headerParams[] = 'realm="' . rawurlencode($realm) . '"';
277
        }
278 4
        foreach ($params as $key => $value) {
279 4
            if (substr_compare($key, 'oauth', 0, 5)) {
280 1
                continue;
281
            }
282 4
            $headerParams[] = rawurlencode((string)$key) . '="' . rawurlencode((string)$value) . '"';
283
        }
284 4
        if (!empty($headerParams)) {
285 4
            $header .= ' ' . implode(', ', $headerParams);
286
        }
287
288 4
        return ['Authorization' => $header];
289
    }
290
291
    /**
292
     * Composes user authorization URL.
293
     *
294
     * @param ServerRequestInterface $incomingRequest
295
     * @param OAuthToken $requestToken OAuth request token.
296
     * @param array $params additional request params.
297
     *
298
     * @return string authorize URL
299
     */
300 1
    public function buildAuthUrl(
301
        ?OAuthToken $requestToken = null,
302
        array $params = []
303
    ) {
304 1
        if (!is_object($requestToken)) {
305
            $requestToken = $this->getState('requestToken');
306
            if (!is_object($requestToken)) {
307
                throw new InvalidArgumentException('Request token is required to build authorize URL!');
308
            }
309
        }
310 1
        $params['oauth_token'] = $requestToken->getToken();
311
312 1
        return RequestUtil::composeUrl($this->authUrl, $params);
313
    }
314
315
    /**
316
     * Fetches OAuth access token.
317
     *
318
     * @param ServerRequestInterface $incomingRequest
319
     * @param string $oauthToken OAuth token returned with redirection back to client.
320
     * @param OAuthToken $requestToken OAuth request token.
321
     * @param string $oauthVerifier OAuth verifier.
322
     * @param array $params additional request params.
323
     *
324
     * @return OAuthToken OAuth access token.
325
     */
326
    public function fetchAccessToken(
327
        ServerRequestInterface $incomingRequest,
328
        string $oauthToken = null,
329
        OAuthToken $requestToken = null,
330
        string $oauthVerifier = null,
331
        array $params = []
332
    ) {
333
        $queryParams = $incomingRequest->getQueryParams();
334
        $bodyParams = $incomingRequest->getParsedBody();
335
        if ($oauthToken === null) {
336
            $oauthToken = $queryParams['oauth_token'] ?? $bodyParams['oauth_token'] ?? null;
337
        }
338
339
        if (!is_object($requestToken)) {
340
            $requestToken = $this->getState('requestToken');
341
            if (!is_object($requestToken)) {
342
                throw new InvalidArgumentException('Request token is required to fetch access token!');
343
            }
344
        }
345
346
        if (strcmp($requestToken->getToken(), $oauthToken) !== 0) {
347
            throw new InvalidArgumentException('Invalid auth state parameter.');
348
        }
349
350
        $this->removeState('requestToken');
351
352
        $defaultParams = [
353
            'oauth_consumer_key' => $this->consumerKey,
354
            'oauth_token' => $requestToken->getToken(),
355
        ];
356
        if ($oauthVerifier === null) {
357
            $oauthVerifier = $queryParams['oauth_verifier'] ?? $bodyParams['oauth_verifier'];
358
        }
359
360
        if (!empty($oauthVerifier)) {
361
            $defaultParams['oauth_verifier'] = $oauthVerifier;
362
        }
363
364
        $request = $this->createRequest(
365
            $this->accessTokenMethod,
366
            RequestUtil::composeUrl($this->accessTokenUrl, array_merge($defaultParams, $params))
367
        );
368
369
        $request = $this->signRequest($request, $requestToken);
370
371
        $request = $this->signRequest($request);
372
        $response = $this->sendRequest($request);
373
374
        $token = $this->createToken(
375
            [
376
                'setParams()' => [Json::decode($response->getBody()->getContents())],
377
            ]
378
        );
379
        $this->setAccessToken($token);
380
381
        return $token;
382
    }
383
384
    public function applyAccessTokenToRequest(RequestInterface $request, OAuthToken $accessToken): RequestInterface
385
    {
386
        $data = RequestUtil::getParams($request);
387
        $data['oauth_consumer_key'] = $this->consumerKey;
388
        $data['oauth_token'] = $accessToken->getToken();
389
        return RequestUtil::addParams($request, $data);
390
    }
391
392
    /**
393
     * Gets new auth token to replace expired one.
394
     *
395
     * @param OAuthToken $token expired auth token.
396
     *
397
     * @return OAuthToken new auth token.
398
     */
399
    public function refreshAccessToken(?OAuthToken $token = null): OAuthToken
400
    {
401
        // @todo
402
        return $token;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $token could return the type null which is incompatible with the type-hinted return Yiisoft\Yii\AuthClient\OAuthToken. Consider adding an additional type-check to rule them out.
Loading history...
403
    }
404
405
    public function getConsumerKey(): string
406
    {
407
        return $this->consumerKey;
408
    }
409
410
    public function setConsumerKey(string $consumerKey): void
411
    {
412
        $this->consumerKey = $consumerKey;
413
    }
414
415
    public function getConsumerSecret(): string
416
    {
417
        return $this->consumerSecret;
418
    }
419
420
    public function setConsumerSecret(string $consumerSecret)
421
    {
422
        $this->consumerSecret = $consumerSecret;
423
    }
424
425
    public function getRequestTokenUrl(): string
426
    {
427
        return $this->requestTokenUrl;
428
    }
429
430 1
    public function setRequestTokenUrl(string $requestTokenUrl): void
431
    {
432 1
        $this->requestTokenUrl = $requestTokenUrl;
433 1
    }
434
435
    public function getRequestTokenMethod(): string
436
    {
437
        return $this->requestTokenMethod;
438
    }
439
440
    public function setRequestTokenMethod(string $requestTokenMethod): void
441
    {
442
        $this->requestTokenMethod = $requestTokenMethod;
443
    }
444
445
    public function getAccessTokenUrl(): string
446
    {
447
        return $this->accessTokenUrl;
448
    }
449
450
    public function setAccessTokenUrl(string $accessTokenUrl): void
451
    {
452
        $this->accessTokenUrl = $accessTokenUrl;
453
    }
454
455
    public function getAccessTokenMethod(): string
456
    {
457
        return $this->accessTokenMethod;
458
    }
459
460
    public function setAccessTokenMethod(string $accessTokenMethod): void
461
    {
462
        $this->accessTokenMethod = $accessTokenMethod;
463
    }
464
465
    public function getAuthorizationHeaderMethods(): ?array
466
    {
467
        return $this->authorizationHeaderMethods;
468
    }
469
470 1
    public function setAuthorizationHeaderMethods(?array $authorizationHeaderMethods = null)
471
    {
472 1
        $this->authorizationHeaderMethods = $authorizationHeaderMethods;
473 1
    }
474
475
    /**
476
     * Composes default {@see returnUrl} value.
477
     *
478
     * @return string return URL.
479
     */
480
    protected function defaultReturnUrl(ServerRequestInterface $request): string
481
    {
482
        $params = $request->getQueryParams();
483
        unset($params['oauth_token']);
484
485
        return $request->getUri()->withQuery(http_build_query($params, '', '&', PHP_QUERY_RFC3986))->__toString();
486
    }
487
}
488