Passed
Push — master ( 13425d...f357db )
by Alexander
02:26
created

OAuth1::buildAuthUrl()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

346
        if (strcmp(/** @scrutinizer ignore-type */ $requestToken->getToken(), $oauthToken) !== 0) {
Loading history...
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): void
421
    {
422
        $this->consumerSecret = $consumerSecret;
423
    }
424
425
    public function getRequestTokenUrl(): string
426
    {
427
        return $this->requestTokenUrl;
428
    }
429
430
    public function setRequestTokenUrl(string $requestTokenUrl): void
431
    {
432
        $this->requestTokenUrl = $requestTokenUrl;
433
    }
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): void
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 (string)$request->getUri()->withQuery(http_build_query($params, '', '&', PHP_QUERY_RFC3986));
486
    }
487
}
488