1
|
|
|
<?php namespace nyx\auth\id\protocols\oauth1\middlewares; |
2
|
|
|
|
3
|
|
|
// External dependencies |
4
|
|
|
use Psr\Http\Message\RequestInterface as Request; |
5
|
|
|
use nyx\utils; |
6
|
|
|
|
7
|
|
|
// Internal dependencies |
8
|
|
|
use nyx\auth\id\protocols\oauth1; |
9
|
|
|
use nyx\auth; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* OAuth 1.0a Request Authorization Middleware |
13
|
|
|
* |
14
|
|
|
* Adds OAuth 1.0a protocol specific parameters to the Request, including its signature. Should be used last |
15
|
|
|
* in a middleware stack as the signature is only generated and valid for the request parameters present at the |
16
|
|
|
* time of its creation. |
17
|
|
|
* |
18
|
|
|
* To invoke this middleware, when it is part of a middleware stack, the $options array passed along with |
19
|
|
|
* the Request must contain an 'oauth1' key that must not be null. It may be an array containing any |
20
|
|
|
* of those optional keys: |
21
|
|
|
* |
22
|
|
|
* - 'signer': an instance of oauth1\interfaces\Signer used to create the Request's signature. |
23
|
|
|
* Note: This field is *mandatory if* the Middleware gets constructed without a default Signer. |
24
|
|
|
* - 'client': an instance of auth\id\credentials\Client containing the client's (consumer's) credentials. |
25
|
|
|
* Note: This field is *mandatory if* the Middleware gets constructed without a set of default client credentials. |
26
|
|
|
* - 'token': a auth\interfaces\Credentials instance representing the OAuth token; |
27
|
|
|
* - 'callback': when true, a "oauth_callback" protocol parameter will be added to the Request, |
28
|
|
|
* based on the Client Credentials given; |
29
|
|
|
* - 'params': an array of additional protocol parameters which will be appended to the |
30
|
|
|
* base protocol parameters (@see gatherAuthorizationParams()). Could be "realm" or other keys |
31
|
|
|
* implemented by a particular Provider; |
32
|
|
|
* |
33
|
|
|
* All other keys are ignored by default. |
34
|
|
|
* |
35
|
|
|
* @package Nyx\Auth |
36
|
|
|
* @version 0.1.0 |
37
|
|
|
* @author Michal Chojnacki <[email protected]> |
38
|
|
|
* @copyright 2012-2017 Nyx Dev Team |
39
|
|
|
* @link https://github.com/unyx/nyx |
40
|
|
|
*/ |
41
|
|
|
class Authorization |
42
|
|
|
{ |
43
|
|
|
/** |
44
|
|
|
* @see http://oauth.net/core/1.0/#consumer_req_param |
45
|
|
|
* Note: Passing the protocol parameters with the body of a Request is currently not implemented by this |
46
|
|
|
* middleware, despite the spec allowing it. |
47
|
|
|
*/ |
48
|
|
|
const METHOD_HEADER = 'header'; |
49
|
|
|
const METHOD_QUERY = 'query'; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var auth\id\credentials\Client The default Client Credentials to use when those are not passed with the options. |
53
|
|
|
*/ |
54
|
|
|
protected $client; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var oauth1\interfaces\Signer The default Signer to use when it is not passed with the options. |
58
|
|
|
*/ |
59
|
|
|
protected $signer; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var string The method with which the protocol parameters will be added to the Request being handled. |
63
|
|
|
*/ |
64
|
|
|
private $method = self::METHOD_HEADER; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Constructs a new OAuth 1.0a Request Authorization Middleware instance. |
68
|
|
|
* |
69
|
|
|
* Note: In a scenario where this handler is used universally in a HTTP Client handler stack that may communicate |
70
|
|
|
* with several different OAuth 1.0a providers, to facilitate reuse of the instance and to avoid potential |
71
|
|
|
* confusion in case of authorization errors, it is suggested to avoid using a default set of client |
72
|
|
|
* credentials and a signer, and instead pass them in along with each Request's $options array. |
73
|
|
|
* |
74
|
|
|
* @param auth\id\credentials\Client $client The default client credentials to sign requests with. |
75
|
|
|
* @param oauth1\interfaces\Signer $signer The default Signer to generate the request's signature with. |
76
|
|
|
*/ |
77
|
|
|
public function __construct(auth\id\credentials\Client $client = null, oauth1\interfaces\Signer $signer = null) |
78
|
|
|
{ |
79
|
|
|
$this->client = $client; |
80
|
|
|
$this->signer = $signer; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Sets the method by which the protocol parameters will be added to the Request. |
85
|
|
|
* |
86
|
|
|
* @param string $method One of the METHOD_* class constants. |
87
|
|
|
* @return $this |
88
|
|
|
* @throws \InvalidArgumentException When attempting to set an unsupported method. |
89
|
|
|
*/ |
90
|
|
|
public function setMethod(string $method) : Authorization |
91
|
|
|
{ |
92
|
|
|
switch($method) { |
93
|
|
|
case self::METHOD_HEADER: |
94
|
|
|
case self::METHOD_QUERY: |
95
|
|
|
$this->method = $method; |
96
|
|
|
return $this; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
throw new \InvalidArgumentException("Authorization method [$method] is not supported by this middleware."); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Invokes the middleware if the $options passed along the Request contain an 'oauth1' key. |
104
|
|
|
* See the class description for which kind of additional options are supported/mandatory. |
105
|
|
|
* |
106
|
|
|
* @param callable $handler |
107
|
|
|
* @return callable |
108
|
|
|
*/ |
109
|
|
|
public function __invoke(callable $handler) : callable |
110
|
|
|
{ |
111
|
|
|
return function ($request, array& $stackOptions) use ($handler) { |
112
|
|
|
|
113
|
|
|
// Skip to the next handler if we weren't asked to do any stuff. |
114
|
|
|
if (!isset($stackOptions['oauth1'])) { |
115
|
|
|
return $handler($request, $stackOptions); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
// We'll be working with references internally and shifting some values around, |
119
|
|
|
// so we might just as well make all the parameters we gather and append more easily accessible |
120
|
|
|
// by pushing into the $stackOptions directly in case there actually *are* handlers in the stack |
121
|
|
|
// that have to do some post-processing after us. |
122
|
|
|
$handlerOptions =& $stackOptions['oauth1'] ?: $stackOptions['oauth1'] = []; |
123
|
|
|
|
124
|
|
|
$this->parseOptions($handlerOptions); |
125
|
|
|
$this->gatherAuthorizationParams($handlerOptions); |
126
|
|
|
$this->sign($request, $handlerOptions); |
127
|
|
|
|
128
|
|
|
// Invoke the next handler with our freshly authorized Request instance. |
129
|
|
|
return $handler($this->handle($request, $handlerOptions), $stackOptions); |
130
|
|
|
}; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Merges and populates the base protocol parameters with any optional protocol parameters passed in the options. |
135
|
|
|
* |
136
|
|
|
* @param array $options A reference to the options passed along with the Request. |
137
|
|
|
* @throws \InvalidArgumentException When no default signer/credentials are available and no valid |
138
|
|
|
* signer/credentials have been given along with the $options. |
139
|
|
|
* @throws \InvalidArgumentException When the 'token' key is given but is not a auth\interfaces\Credentials instance. |
140
|
|
|
*/ |
141
|
|
|
protected function parseOptions(array& $options) |
142
|
|
|
{ |
143
|
|
|
// Ensure we got a valid 'signer' key. |
144
|
|
View Code Duplication |
if ((null === $this->signer && !isset($options['signer'])) || (isset($options['signer']) && !$options['signer'] instanceof oauth1\interfaces\Signer)) { |
|
|
|
|
145
|
|
|
throw new \InvalidArgumentException('A [signer] key with a Signer implementing '.oauth1\interfaces\Signer::class.' must be provided.'); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
// Ensure the 'client' is set and contains valid Credentials. |
149
|
|
View Code Duplication |
if ((null === $this->client && !isset($options['client'])) || (isset($options['client']) && !$options['client'] instanceof auth\id\credentials\Client)) { |
|
|
|
|
150
|
|
|
throw new \InvalidArgumentException('A [client] key with a Credentials implementing '.auth\id\credentials\Client::class.' must be provided.'); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
// Ensure the 'params' key is always set. |
154
|
|
|
if (!isset($options['params'])) { |
155
|
|
|
$options['params'] = []; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
// If the 'token' optional key is present, we'll automatically push it's id into the authorization params |
159
|
|
|
// and use its secret in the signing process. Provided it's of the appropriate type. |
160
|
|
|
if (isset($options['token'])) { |
161
|
|
|
if (!$options['token'] instanceof auth\interfaces\Credentials) { |
162
|
|
|
throw new \InvalidArgumentException('The [token] key, if provided, must be an instance of '.auth\interfaces\Credentials::class.'.'); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
$options['params']['oauth_token'] = $options['token']->getId(); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
// If the 'callback' optional key is present and true, we'll set the oauth_callback authorization parameter |
169
|
|
|
// automatically, based on the consumer's redirect URI. |
170
|
|
|
if (isset($options['callback']) && true === $options['callback']) { |
171
|
|
|
$options['params']['oauth_callback'] = isset($options['client']) ? $options['client']->getRedirectUri() : $this->client->getRedirectUri(); |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Merges and populates the base protocol parameters with any optional protocol parameters passed in the options. |
177
|
|
|
* |
178
|
|
|
* @param array $options A reference to the options passed along with the Request. |
179
|
|
|
*/ |
180
|
|
|
protected function gatherAuthorizationParams(array& $options) |
181
|
|
|
{ |
182
|
|
|
// The signature must not be included in any base string - we'll generate the signature for the current |
183
|
|
|
// parameters in a moment anyways. |
184
|
|
|
// @see https://oauth.net/core/1.0/#anchor14 (Spec #9.1.1) |
185
|
|
|
unset($options['params']['oauth_signature']); |
186
|
|
|
|
187
|
|
|
// Unite our base parameters (which cannot be overridden) with the optional ones passed in. |
188
|
|
|
$options['params'] = [ |
189
|
|
|
'oauth_version' => '1.0', |
190
|
|
|
'oauth_consumer_key' => isset($options['client']) ? $options['client']->getId() : $this->client->getId(), |
191
|
|
|
'oauth_signature_method' => isset($options['signer']) ? $options['signer']->getMethod() : $this->signer->getMethod(), |
192
|
|
|
'oauth_nonce' => utils\Random::string(6, utils\str\Character::CHARS_BASE64, utils\Random::STRENGTH_NONE), |
193
|
|
|
'oauth_timestamp' => time(), |
194
|
|
|
] + $options['params']; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Generates the Request's signature based on the defined parameters. |
199
|
|
|
* |
200
|
|
|
* @param Request $request The Request to sign. |
201
|
|
|
* @param array $options A reference to the options passed along with the Request. |
202
|
|
|
*/ |
203
|
|
|
protected function sign(Request $request, array& $options) |
204
|
|
|
{ |
205
|
|
|
$signer = $options['signer'] ?? $this->signer; |
206
|
|
|
|
207
|
|
|
$options['params']['oauth_signature'] = $signer->sign( |
208
|
|
|
$request, |
209
|
|
|
$options['params'], |
210
|
|
|
$options['client'] ?? $this->client, |
211
|
|
|
$options['token'] ?? null |
212
|
|
|
); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Handles the Request. |
217
|
|
|
* |
218
|
|
|
* @param Request $request The request. |
219
|
|
|
* @param array $options The options passed along with the Request. |
220
|
|
|
* @return Request |
221
|
|
|
* @throws \InvalidArgumentException When an invalid authorization method is set. |
222
|
|
|
*/ |
223
|
|
|
protected function handle(Request $request, array $options) : Request |
224
|
|
|
{ |
225
|
|
|
switch ($this->method) { |
226
|
|
|
case self::METHOD_HEADER: |
|
|
|
|
227
|
|
|
return $this->withHeader($request, $options['params']); |
228
|
|
|
break; |
|
|
|
|
229
|
|
|
|
230
|
|
|
case self::METHOD_QUERY: |
|
|
|
|
231
|
|
|
return $this->withQuery($request, $options['params']); |
232
|
|
|
break; |
|
|
|
|
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
// Should never happen since the 'method' property is private and we're doing the checking in setMethod() |
236
|
|
|
// But... |
237
|
|
|
throw new \InvalidArgumentException("Authorization method [$this->method] is not supported by this middleware."); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Creates a new Request with the Authorization protocol header applied to it. |
242
|
|
|
* |
243
|
|
|
* @param Request $request The base Request to add the header to. |
244
|
|
|
* @param array $params The protocol parameters. |
245
|
|
|
* @return Request |
246
|
|
|
*/ |
247
|
|
|
protected function withHeader(Request $request, array $params) : Request |
248
|
|
|
{ |
249
|
|
|
// Percent encode the Authorization header parameters according to spec. |
250
|
|
|
foreach ($params as $key => $value) { |
251
|
|
|
$params[$key] = $key.'="'.rawurlencode($value).'"'; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
return $request->withHeader('Authorization', 'OAuth ' . implode(', ', $params)); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Creates a new Request with the protocol parameters appended to its query string. |
259
|
|
|
* |
260
|
|
|
* @param Request $request The base Request to add the query string to. |
261
|
|
|
* @param array $params The protocol parameters. |
262
|
|
|
* @return Request |
263
|
|
|
*/ |
264
|
|
|
protected function withQuery(Request $request, array $params) : Request |
265
|
|
|
{ |
266
|
|
|
$uri = $request->getUri(); |
267
|
|
|
|
268
|
|
|
parse_str($uri->getQuery(), $current); |
269
|
|
|
|
270
|
|
|
return $request->withUri($uri->withQuery(http_build_query($params + $current, '', '&', PHP_QUERY_RFC3986))); |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.