Passed
Push — master ( 67aa25...a90a56 )
by Alexander
08:11
created

OAuth1::composeAuthorizationHeader()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

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