Completed
Push — master ( cfd0e6...d7683b )
by Michał
06:41
created

Authorization::setMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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']->getSignatureMethod() : $this->signer->getSignatureMethod(),
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
201
     */
202
    protected function sign(Request $request, array& $options)
203
    {
204
        $signer = $options['signer'] ?? $this->signer;
205
206
        $options['params']['oauth_signature'] = $signer->sign(
207
            $request,
208
            $options['params'],
209
            $options['client'] ?? $this->client,
210
            $options['token']  ?? null
211
        );
212
    }
213
214
    /**
215
     * Handles the Request.
216
     *
217
     * @param   Request     $request        The request.
218
     * @param   array       $options        The options passed along with the Request.
219
     * @return  Request
220
     * @throws  \InvalidArgumentException   When an invalid authorization method is set.
221
     */
222
    protected function handle(Request $request, array $options) : Request
223
    {
224
        switch ($this->method) {
225
            case self::METHOD_HEADER:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
226
                return $this->withHeader($request, $options['params']);
227
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
228
229
            case self::METHOD_QUERY:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
230
                return $this->withQuery($request, $options['params']);
231
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
232
        }
233
234
        // Should never happen since the 'method' property is private and we're doing the checking in setMethod()
235
        // But...
236
        throw new \InvalidArgumentException("Authorization method [$this->method] is not supported by this middleware.");
237
    }
238
239
    /**
240
     * Creates a new Request with the Authorization protocol header applied to it.
241
     *
242
     * @param   Request $request    The base Request to add the header to.
243
     * @param   array   $params     The protocol parameters.
244
     * @return  Request
245
     */
246
    protected function withHeader(Request $request, array $params) : Request
247
    {
248
        // Percent encode the Authorization header parameters according to spec.
249
        foreach ($params as $key => $value) {
250
            $params[$key] = $key.'="'.rawurlencode($value).'"';
251
        }
252
253
        return $request->withHeader('Authorization', 'OAuth ' . implode(', ', $params));
254
    }
255
256
    /**
257
     * Creates a new Request with the protocol parameters appended to its query string.
258
     *
259
     * @param   Request $request    The base Request to add the query string to.
260
     * @param   array   $params     The protocol parameters.
261
     * @return  Request
262
     */
263
    protected function withQuery(Request $request, array $params) : Request
264
    {
265
        $uri = $request->getUri();
266
267
        parse_str($uri->getQuery(), $current);
268
269
        return $request->withUri($uri->withQuery(http_build_query($params + $current, '', '&', PHP_QUERY_RFC3986)));
270
    }
271
}
272