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

OAuth1   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 449
Duplicated Lines 0 %

Test Coverage

Coverage 59.51%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 151
c 1
b 0
f 0
dl 0
loc 449
ccs 100
cts 168
cp 0.5951
rs 7.92
wmc 51

27 Methods

Rating   Name   Duplication   Size   Complexity  
B signRequest() 0 51 10
A getAccessTokenUrl() 0 3 1
B fetchAccessToken() 0 56 7
A generateTimestamp() 0 3 1
A defaultReturnUrl() 0 6 1
A getAccessTokenMethod() 0 3 1
A setAccessTokenUrl() 0 3 1
A setAuthorizationHeaderMethods() 0 3 1
A getConsumerSecret() 0 3 1
A generateCommonRequestParams() 0 6 1
A getConsumerKey() 0 3 1
A refreshAccessToken() 0 4 1
A setRequestTokenMethod() 0 3 1
A setAccessTokenMethod() 0 3 1
A buildAuthUrl() 0 8 1
A getAuthorizationHeaderMethods() 0 3 1
A getRequestTokenMethod() 0 3 1
A composeAuthorizationHeader() 0 18 5
A generateNonce() 0 3 1
A getRequestTokenUrl() 0 3 1
A composeSignatureKey() 0 18 3
A setConsumerKey() 0 3 1
A setRequestTokenUrl() 0 3 1
A applyAccessTokenToRequest() 0 6 1
A composeSignatureBaseString() 0 20 2
A setConsumerSecret() 0 3 1
A fetchRequestToken() 0 32 3

How to fix   Complexity   

Complex Class

Complex classes like OAuth1 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OAuth1, and based on these observations, apply Extract Interface, too.

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