Passed
Pull Request — master (#33)
by Anton
02:36
created

OAuth1::signRequest()   B

Complexity

Conditions 10
Paths 7

Size

Total Lines 51
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 10.0045

Importance

Changes 0
Metric Value
cc 10
eloc 29
c 0
b 0
f 0
nc 7
nop 2
dl 0
loc 51
ccs 27
cts 28
cp 0.9643
crap 10.0045
rs 7.6666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 OAuth
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
     * Composes user authorization URL.
70
     *
71
     * @param ServerRequestInterface $incomingRequest
72
     * @param array $params additional request params.
73
     *
74
     * @return string authorize URL
75
     */
76 1
    public function buildAuthUrl(
77
        ServerRequestInterface $incomingRequest,
78
        array $params = []
79
    ): string {
80 1
        $requestToken = $this->fetchRequestToken($incomingRequest);
81 1
        $params['oauth_token'] = $requestToken->getToken();
82
83 1
        return RequestUtil::composeUrl($this->authUrl, $params);
84
    }
85
86
    /**
87
     * Fetches the OAuth request token.
88
     *
89
     * @param ServerRequestInterface $incomingRequest
90
     * @param array $params additional request params.
91
     *
92
     * @throws \Yiisoft\Factory\Exception\InvalidConfigException
93
     *
94
     * @return OAuthToken request token.
95
     */
96 1
    public function fetchRequestToken(ServerRequestInterface $incomingRequest, array $params = []): OAuthToken
97
    {
98 1
        $this->setAccessToken(null);
99
        $defaultParams = [
100 1
            'oauth_consumer_key' => $this->consumerKey,
101 1
            'oauth_callback' => $this->getReturnUrl($incomingRequest),
102 1
            'xoauth_displayname' => $incomingRequest->getAttribute(AuthAction::AUTH_NAME),
103
        ];
104 1
        if (!empty($this->getScope())) {
105
            $defaultParams['scope'] = $this->getScope();
106
        }
107
108 1
        $request = $this->createRequest(
109 1
            $this->requestTokenMethod,
110 1
            $this->requestTokenUrl . '?' . http_build_query(
111 1
                array_merge($defaultParams, $params)
112
            )
113
        );
114
115 1
        $request = $this->signRequest($request);
116 1
        $response = $this->sendRequest($request);
117
118 1
        $tokenConfig = Json::decode((string) $response->getBody());
119
120 1
        if (empty($tokenConfig)) {
121
            throw new InvalidArgumentException('Request token is required to build authorize URL!');
122
        }
123
124 1
        $token = $this->createToken($tokenConfig);
125 1
        $this->setState('requestToken', $token);
126
127 1
        return $token;
128
    }
129
130
    /**
131
     * Sign given request with {@see signatureMethod}.
132
     *
133
     * @param RequestInterface $request request instance.
134
     * @param OAuthToken|null $token OAuth token to be used for signature, if not set {@see accessToken} will be used.
135
     *
136
     * @return RequestInterface
137
     */
138 3
    public function signRequest(RequestInterface $request, ?OAuthToken $token = null): RequestInterface
139
    {
140 3
        $params = RequestUtil::getParams($request);
141
142 3
        if (isset($params['oauth_signature_method']) || $request->hasHeader('authorization')) {
143
            // avoid double sign of request
144
            return $request;
145
        }
146
147 3
        if (empty($request->getUri()->getQuery())) {
148 1
            $params = $this->generateCommonRequestParams();
149
        } else {
150 2
            $params = array_merge($this->generateCommonRequestParams(), $params);
151
        }
152
153 3
        $url = (string)$request->getUri();
154
155 3
        $signatureMethod = $this->getSignatureMethod();
156
157 3
        $params['oauth_signature_method'] = $signatureMethod->getName();
158 3
        $signatureBaseString = $this->composeSignatureBaseString($request->getMethod(), $url, $params);
159 3
        $signatureKey = $this->composeSignatureKey($token);
160 3
        $params['oauth_signature'] = $signatureMethod->generateSignature($signatureBaseString, $signatureKey);
161
162
        if (
163 3
            $this->authorizationHeaderMethods === null || in_array(
164 3
                strtoupper($request->getMethod()),
165 3
                array_map(
166 3
                    'strtoupper',
167 3
                    $this->authorizationHeaderMethods
168
                ),
169 3
                true
170
            )
171
        ) {
172 1
            $authorizationHeader = $this->composeAuthorizationHeader($params);
173 1
            if (!empty($authorizationHeader)) {
174 1
                foreach ($authorizationHeader as $name => $value) {
175 1
                    $request = $request->withHeader($name, $value);
176
                }
177
178
                // removing authorization header params, avoiding duplicate param server error :
179 1
                foreach ($params as $key => $value) {
180 1
                    if (substr_compare($key, 'oauth', 0, 5) === 0) {
181 1
                        unset($params[$key]);
182
                    }
183
                }
184
            }
185
        }
186
187 3
        $uri = $request->getUri()->withQuery(http_build_query($params));
188 3
        return $request->withUri($uri);
189
    }
190
191
    /**
192
     * Generate common request params like version, timestamp etc.
193
     *
194
     * @return array common request params.
195
     */
196 3
    protected function generateCommonRequestParams(): array
197
    {
198
        return [
199 3
            'oauth_version' => self::PROTOCOL_VERSION,
200 3
            'oauth_nonce' => $this->generateNonce(),
201 3
            'oauth_timestamp' => $this->generateTimestamp(),
202
        ];
203
    }
204
205
    /**
206
     * Generates nonce value.
207
     *
208
     * @return string nonce value.
209
     */
210 3
    protected function generateNonce(): string
211
    {
212 3
        return md5(microtime() . mt_rand());
213
    }
214
215
    /**
216
     * Generates timestamp.
217
     *
218
     * @return int timestamp.
219
     */
220 3
    protected function generateTimestamp(): int
221
    {
222 3
        return time();
223
    }
224
225
    /**
226
     * Creates signature base string, which will be signed by {@see signatureMethod}.
227
     *
228
     * @param string $method request method.
229
     * @param string $url request URL.
230
     * @param array $params request params.
231
     *
232
     * @return string base signature string.
233
     */
234 3
    protected function composeSignatureBaseString($method, $url, array $params)
235
    {
236 3
        if (strpos($url, '?') !== false) {
237 2
            [$url, $queryString] = explode('?', $url, 2);
238 2
            parse_str($queryString, $urlParams);
239 2
            $params = array_merge($urlParams, $params);
240
        }
241 3
        unset($params['oauth_signature']);
242 3
        uksort(
243 3
            $params,
244 3
            'strcmp'
245
        ); // Parameters are sorted by name, using lexicographical byte value ordering. Ref: Spec: 9.1.1
246
        $parts = [
247 3
            strtoupper($method),
248 3
            $url,
249 3
            http_build_query($params, '', '&', PHP_QUERY_RFC3986),
250
        ];
251 3
        $parts = array_map('rawurlencode', $parts);
252
253 3
        return implode('&', $parts);
254
    }
255
256
    /**
257
     * Composes request signature key.
258
     *
259
     * @param OAuthToken|null $token OAuth token to be used for signature key.
260
     *
261
     * @return string signature key.
262
     */
263 3
    protected function composeSignatureKey($token = null): string
264
    {
265
        $signatureKeyParts = [
266 3
            $this->consumerSecret,
267
        ];
268
269 3
        if ($token === null) {
270 3
            $token = $this->getAccessToken();
271
        }
272 3
        if (is_object($token)) {
273
            $signatureKeyParts[] = $token->getTokenSecret();
274
        } else {
275 3
            $signatureKeyParts[] = '';
276
        }
277
278 3
        $signatureKeyParts = array_map('rawurlencode', $signatureKeyParts);
279
280 3
        return implode('&', $signatureKeyParts);
281
    }
282
283
    /**
284
     * Composes authorization header.
285
     *
286
     * @param array $params request params.
287
     * @param string $realm authorization realm.
288
     *
289
     * @return array authorization header in format: [name => content].
290
     */
291 4
    public function composeAuthorizationHeader(array $params, $realm = '')
292
    {
293 4
        $header = 'OAuth';
294 4
        $headerParams = [];
295 4
        if (!empty($realm)) {
296 1
            $headerParams[] = 'realm="' . rawurlencode($realm) . '"';
297
        }
298 4
        foreach ($params as $key => $value) {
299 4
            if (substr_compare($key, 'oauth', 0, 5)) {
300 1
                continue;
301
            }
302 4
            $headerParams[] = rawurlencode((string)$key) . '="' . rawurlencode((string)$value) . '"';
303
        }
304 4
        if (!empty($headerParams)) {
305 4
            $header .= ' ' . implode(', ', $headerParams);
306
        }
307
308 4
        return ['Authorization' => $header];
309
    }
310
311
    /**
312
     * Fetches OAuth access token.
313
     *
314
     * @param ServerRequestInterface $incomingRequest
315
     * @param string|null $oauthToken OAuth token returned with redirection back to client.
316
     * @param OAuthToken|null $requestToken OAuth request token.
317
     * @param string|null $oauthVerifier OAuth verifier.
318
     * @param array $params additional request params.
319
     *
320
     * @return OAuthToken OAuth access token.
321
     */
322
    public function fetchAccessToken(
323
        ServerRequestInterface $incomingRequest,
324
        string $oauthToken = null,
325
        OAuthToken $requestToken = null,
326
        string $oauthVerifier = null,
327
        array $params = []
328
    ): OAuthToken {
329
        $queryParams = $incomingRequest->getQueryParams();
330
        $bodyParams = $incomingRequest->getParsedBody();
331
        if ($oauthToken === null) {
332
            $oauthToken = $queryParams['oauth_token'] ?? $bodyParams['oauth_token'] ?? null;
333
        }
334
335
        if (!is_object($requestToken)) {
336
            $requestToken = $this->getState('requestToken');
337
            if (!is_object($requestToken)) {
338
                throw new InvalidArgumentException('Request token is required to fetch access token!');
339
            }
340
        }
341
342
        if (strcmp($requestToken->getToken(), $oauthToken) !== 0) {
0 ignored issues
show
Bug introduced by
It seems like $requestToken->getToken() can also be of type null; however, parameter $string1 of strcmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

342
        if (strcmp(/** @scrutinizer ignore-type */ $requestToken->getToken(), $oauthToken) !== 0) {
Loading history...
343
            throw new InvalidArgumentException('Invalid auth state parameter.');
344
        }
345
346
        $this->removeState('requestToken');
347
348
        $defaultParams = [
349
            'oauth_consumer_key' => $this->consumerKey,
350
            'oauth_token' => $requestToken->getToken(),
351
        ];
352
        if ($oauthVerifier === null) {
353
            $oauthVerifier = $queryParams['oauth_verifier'] ?? $bodyParams['oauth_verifier'];
354
        }
355
356
        if (!empty($oauthVerifier)) {
357
            $defaultParams['oauth_verifier'] = $oauthVerifier;
358
        }
359
360
        $request = $this->createRequest(
361
            $this->accessTokenMethod,
362
            RequestUtil::composeUrl($this->accessTokenUrl, array_merge($defaultParams, $params))
363
        );
364
365
        $request = $this->signRequest($request, $requestToken);
366
367
        $request = $this->signRequest($request);
368
        $response = $this->sendRequest($request);
369
370
        $token = $this->createToken(
371
            [
372
                'setParams()' => [Json::decode($response->getBody()->getContents())],
373
            ]
374
        );
375
        $this->setAccessToken($token);
376
377
        return $token;
378
    }
379
380
    public function applyAccessTokenToRequest(RequestInterface $request, OAuthToken $accessToken): RequestInterface
381
    {
382
        $data = RequestUtil::getParams($request);
383
        $data['oauth_consumer_key'] = $this->consumerKey;
384
        $data['oauth_token'] = $accessToken->getToken();
385
        return RequestUtil::addParams($request, $data);
386
    }
387
388
    /**
389
     * Gets new auth token to replace expired one.
390
     *
391
     * @param OAuthToken|null $token expired auth token.
392
     *
393
     * @return OAuthToken new auth token.
394
     */
395
    public function refreshAccessToken(?OAuthToken $token = null): OAuthToken
396
    {
397
        // @todo
398
        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...
399
    }
400
401
    public function getConsumerKey(): string
402
    {
403
        return $this->consumerKey;
404
    }
405
406
    public function setConsumerKey(string $consumerKey): void
407
    {
408
        $this->consumerKey = $consumerKey;
409
    }
410
411
    public function getConsumerSecret(): string
412
    {
413
        return $this->consumerSecret;
414
    }
415
416
    public function setConsumerSecret(string $consumerSecret): void
417
    {
418
        $this->consumerSecret = $consumerSecret;
419
    }
420
421
    public function getRequestTokenUrl(): string
422
    {
423
        return $this->requestTokenUrl;
424
    }
425
426 1
    public function setRequestTokenUrl(string $requestTokenUrl): void
427
    {
428 1
        $this->requestTokenUrl = $requestTokenUrl;
429 1
    }
430
431
    public function getRequestTokenMethod(): string
432
    {
433
        return $this->requestTokenMethod;
434
    }
435
436
    public function setRequestTokenMethod(string $requestTokenMethod): void
437
    {
438
        $this->requestTokenMethod = $requestTokenMethod;
439
    }
440
441
    public function getAccessTokenUrl(): string
442
    {
443
        return $this->accessTokenUrl;
444
    }
445
446
    public function setAccessTokenUrl(string $accessTokenUrl): void
447
    {
448
        $this->accessTokenUrl = $accessTokenUrl;
449
    }
450
451
    public function getAccessTokenMethod(): string
452
    {
453
        return $this->accessTokenMethod;
454
    }
455
456
    public function setAccessTokenMethod(string $accessTokenMethod): void
457
    {
458
        $this->accessTokenMethod = $accessTokenMethod;
459
    }
460
461
    public function getAuthorizationHeaderMethods(): ?array
462
    {
463
        return $this->authorizationHeaderMethods;
464
    }
465
466 1
    public function setAuthorizationHeaderMethods(?array $authorizationHeaderMethods = null): void
467
    {
468 1
        $this->authorizationHeaderMethods = $authorizationHeaderMethods;
469 1
    }
470
471
    /**
472
     * Composes default {@see returnUrl} value.
473
     *
474
     * @return string return URL.
475
     */
476 1
    protected function defaultReturnUrl(ServerRequestInterface $request): string
477
    {
478 1
        $params = $request->getQueryParams();
479 1
        unset($params['oauth_token']);
480
481 1
        return (string)$request->getUri()->withQuery(http_build_query($params, '', '&', PHP_QUERY_RFC3986));
482
    }
483
}
484