OAuth1Provider::parseTokenResponse()   B
last analyzed

Complexity

Conditions 8
Paths 5

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
c 0
b 0
f 0
dl 0
loc 33
rs 8.4444
cc 8
nc 5
nop 2
1
<?php
2
/**
3
 * Class OAuth1Provider
4
 *
5
 * @link https://tools.ietf.org/html/rfc5849
6
 *
7
 * @created      09.07.2017
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2017 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\OAuth\Core;
14
15
use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil};
16
use Psr\Http\Message\{RequestInterface, ResponseInterface, UriInterface};
17
use function array_map;
18
use function array_merge;
19
use function base64_encode;
20
use function hash_hmac;
21
use function implode;
22
use function in_array;
23
use function random_bytes;
24
use function sodium_bin2hex;
25
use function strtoupper;
26
use function time;
27
28
/**
29
 * Implements an abstract OAuth1 provider with all methods required by the OAuth1Interface.
30
 */
31
abstract class OAuth1Provider extends OAuthProvider implements OAuth1Interface{
32
33
	/**
34
	 * The request OAuth1 token URL
35
	 */
36
	protected string $requestTokenURL;
37
38
	/**
39
	 * @inheritDoc
40
	 */
41
	public function getAuthURL(array $params = null):UriInterface{
42
		$params = array_merge(($params ?? []), ['oauth_token' => $this->getRequestToken()->accessToken]);
43
44
		return $this->uriFactory->createUri(QueryUtil::merge($this->authURL, $params));
45
	}
46
47
	/**
48
	 * @inheritDoc
49
	 */
50
	public function getRequestToken():AccessToken{
51
52
		$params = [
53
			'oauth_callback'         => $this->options->callbackURL,
0 ignored issues
show
Bug Best Practice introduced by
The property $callbackURL is declared protected in chillerlan\OAuth\OAuthOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
54
			'oauth_consumer_key'     => $this->options->key,
0 ignored issues
show
Bug Best Practice introduced by
The property $key is declared protected in chillerlan\OAuth\OAuthOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
55
			'oauth_nonce'            => $this->nonce(),
56
			'oauth_signature_method' => 'HMAC-SHA1',
57
			'oauth_timestamp'        => time(),
58
			'oauth_version'          => '1.0',
59
		];
60
61
		$params['oauth_signature'] = $this->getSignature($this->requestTokenURL, $params, 'POST');
62
63
		$request = $this->requestFactory
64
			->createRequest('POST', $this->requestTokenURL)
65
			->withHeader('Authorization', 'OAuth '.QueryUtil::build($params, null, ', ', '"'))
66
			->withHeader('Accept-Encoding', 'identity') // try to avoid compression
67
			->withHeader('Content-Length', '0') // tumblr requires a content-length header set
68
		;
69
70
		foreach($this->authHeaders as $header => $value){
71
			$request = $request->withAddedHeader($header, $value);
72
		}
73
74
		return $this->parseTokenResponse($this->http->sendRequest($request), true);
75
	}
76
77
	/**
78
	 * Parses the response from a request to the token endpoint
79
	 *
80
	 * @link https://tools.ietf.org/html/rfc5849#section-2.1
81
	 *
82
	 * @throws \chillerlan\OAuth\Core\ProviderException
83
	 */
84
	protected function parseTokenResponse(ResponseInterface $response, bool $checkCallbackConfirmed = null):AccessToken{
85
		$data = QueryUtil::parse(MessageUtil::decompress($response));
86
87
		if(empty($data)){
88
			throw new ProviderException('unable to parse token response');
89
		}
90
		elseif(isset($data['error'])){
91
			throw new ProviderException('error retrieving access token: '.$data['error']);
92
		}
93
		elseif(!isset($data['oauth_token']) || !isset($data['oauth_token_secret'])){
94
			throw new ProviderException('invalid token');
95
		}
96
97
		if(
98
			$checkCallbackConfirmed
99
			&& (!isset($data['oauth_callback_confirmed']) || $data['oauth_callback_confirmed'] !== 'true')
100
		){
101
			throw new ProviderException('oauth callback unconfirmed');
102
		}
103
104
		$token = $this->createAccessToken();
105
106
		$token->accessToken       = $data['oauth_token'];
107
		$token->accessTokenSecret = $data['oauth_token_secret'];
108
		$token->expires           = AccessToken::EOL_NEVER_EXPIRES;
109
110
		unset($data['oauth_token'], $data['oauth_token_secret']);
111
112
		$token->extraParams = $data;
113
114
		$this->storage->storeAccessToken($token, $this->serviceName);
115
116
		return $token;
117
	}
118
119
	/**
120
	 * returns a 32 byte random string (in hexadecimal representation) for use as a nonce
121
	 */
122
	protected function nonce():string{
123
		return sodium_bin2hex(random_bytes(32));
124
	}
125
126
	/**
127
	 * Generates a request signature
128
	 *
129
	 * @link https://tools.ietf.org/html/rfc5849#section-3.4
130
	 *
131
	 * @throws \chillerlan\OAuth\Core\ProviderException
132
	 */
133
	protected function getSignature(string $url, array $params, string $method, string $accessTokenSecret = null):string{
134
		$parsed = QueryUtil::parseUrl($url);
135
136
		if(!isset($parsed['host']) || !isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)){
137
			throw new ProviderException('getSignature: invalid url');
138
		}
139
140
		$query           = QueryUtil::parse(($parsed['query'] ?? ''));
141
		$signatureParams = array_merge($query, $params);
142
143
		unset($signatureParams['oauth_signature']);
144
145
		// https://tools.ietf.org/html/rfc5849#section-3.4.1.1
146
		$data = array_map('rawurlencode', [
147
			strtoupper(($method ?? 'POST')),
148
			$parsed['scheme'].'://'.$parsed['host'].($parsed['path'] ?? ''),
149
			QueryUtil::build($signatureParams),
150
		]);
151
152
		// https://tools.ietf.org/html/rfc5849#section-3.4.2
153
		$key  = array_map('rawurlencode', [
154
			$this->options->secret,
0 ignored issues
show
Bug Best Practice introduced by
The property $secret is declared protected in chillerlan\OAuth\OAuthOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
155
			($accessTokenSecret ?? ''),
156
		]);
157
158
		return base64_encode(hash_hmac('sha1', implode('&', $data), implode('&', $key), true));
159
	}
160
161
	/**
162
	 * @inheritDoc
163
	 */
164
	public function getAccessToken(string $token, string $verifier):AccessToken{
165
166
		$request = $this->requestFactory
167
			->createRequest('POST', QueryUtil::merge($this->accessTokenURL, ['oauth_verifier' => $verifier]))
168
			->withHeader('Accept-Encoding', 'identity')
169
			->withHeader('Content-Length', '0')
170
		;
171
172
		$request = $this->getRequestAuthorization($request, $this->storage->getAccessToken($this->serviceName));
173
174
		return $this->parseTokenResponse($this->http->sendRequest($request));
175
	}
176
177
	/**
178
	 * @inheritDoc
179
	 */
180
	public function getRequestAuthorization(RequestInterface $request, AccessToken $token):RequestInterface{
181
		$uri   = $request->getUri();
182
		$query = QueryUtil::parse($uri->getQuery());
183
184
		$parameters = [
185
			'oauth_consumer_key'     => $this->options->key,
0 ignored issues
show
Bug Best Practice introduced by
The property $key is declared protected in chillerlan\OAuth\OAuthOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
186
			'oauth_nonce'            => $this->nonce(),
187
			'oauth_signature_method' => 'HMAC-SHA1',
188
			'oauth_timestamp'        => time(),
189
			'oauth_token'            => $token->accessToken,
190
			'oauth_version'          => '1.0',
191
		];
192
193
		$parameters['oauth_signature'] = $this->getSignature(
194
			(string)$uri->withQuery('')->withFragment(''),
195
			array_merge($query, $parameters),
196
			$request->getMethod(),
197
			$token->accessTokenSecret
198
		);
199
200
		if(isset($query['oauth_session_handle'])){
201
			$parameters['oauth_session_handle'] = $query['oauth_session_handle']; // @codeCoverageIgnore
202
		}
203
204
		return $request->withHeader('Authorization', 'OAuth '.QueryUtil::build($parameters, null, ', ', '"'));
205
	}
206
207
}
208