Issues (25)

src/OAuth1.php (2 issues)

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

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