Authorization   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 232
Duplicated Lines 2.59 %

Coupling/Cohesion

Components 2
Dependencies 6

Importance

Changes 0
Metric Value
wmc 32
lcom 2
cbo 6
dl 6
loc 232
rs 9.6
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A setMethod() 0 11 3
A __invoke() 0 23 3
C parseOptions() 6 33 15
A gatherAuthorizationParams() 0 16 3
A sign() 0 11 1
A handle() 0 16 3
A withHeader() 0 9 2
A withQuery() 0 8 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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']->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:
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...
227
                return $this->withHeader($request, $options['params']);
228
                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...
229
230
            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...
231
                return $this->withQuery($request, $options['params']);
232
                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...
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