Oauth   C
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 502
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 1
dl 0
loc 502
rs 5.6445
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A authorize() 0 18 3
A getProvider() 0 10 2
A getProviders() 0 12 2
C getQueryAuth() 0 32 8
B getQueryToken() 0 19 5
A getAuthUrl() 0 13 3
A encodeState() 0 16 2
A decodeState() 0 16 3
A setState() 0 4 1
A getState() 0 4 1
A unsetState() 0 10 2
A setToken() 0 8 2
A isValidToken() 0 6 3
A isValidState() 0 4 1
A getToken() 0 4 1
B requestToken() 0 24 3
A exchangeToken() 0 19 4
C exchangeTokenServer() 0 27 7
A encodeJwt() 0 9 2
A callHandler() 0 5 1
D prepareProviders() 0 26 10
A decodeJson() 0 10 2
A __construct() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like Oauth often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Oauth, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @package Oauth
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2017, Iurii Makukh
7
 * @license https://www.gnu.org/licenses/gpl.html GNU/GPLv3
8
 */
9
10
namespace gplcart\modules\oauth\models;
11
12
use Exception;
13
use gplcart\core\Handler;
14
use gplcart\core\helpers\Session;
15
use gplcart\core\helpers\Url;
16
use gplcart\core\Hook;
17
use gplcart\core\models\Http;
18
use gplcart\modules\oauth\helpers\Jwt;
19
use OutOfRangeException;
20
use UnexpectedValueException;
21
22
/**
23
 * Manages basic behaviors and data related to Oauth 2.0 functionality
24
 */
25
class Oauth
26
{
27
28
    /**
29
     * JWT helper
30
     * @var \gplcart\modules\oauth\helpers\Jwt $jwt
31
     */
32
    protected $jwt;
33
34
    /**
35
     * Hook class instance
36
     * @var \gplcart\core\Hook $hook
37
     */
38
    protected $hook;
39
40
    /**
41
     * Http model class instance
42
     * @var \gplcart\core\models\Http $http
43
     */
44
    protected $http;
45
46
    /**
47
     * URL helper instance
48
     * @var \gplcart\core\helpers\Url $url
49
     */
50
    protected $url;
51
52
    /**
53
     * Session helper instance
54
     * @var \gplcart\core\helpers\Session $session
55
     */
56
    protected $session;
57
58
    /**
59
     * Oauth constructor.
60
     * @param Hook $hook
61
     * @param Http $http
62
     * @param Jwt $jwt
63
     * @param Session $session
64
     * @param Url $url
65
     */
66
    public function __construct(Hook $hook, Http $http, Jwt $jwt, Session $session, Url $url)
67
    {
68
        $this->jwt = $jwt;
69
        $this->url = $url;
70
        $this->hook = $hook;
71
        $this->http = $http;
72
        $this->session = $session;
73
    }
74
75
    /**
76
     * Does main authorization process
77
     * @param array $provider
78
     * @param array $params
79
     * @return array
80
     */
81
    public function authorize(array $provider, array $params)
82
    {
83
        $result = null;
84
        $this->hook->attach('module.oauth.authorize.before', $provider, $params, $result, $this);
85
86
        if (isset($result)) {
87
            return $result;
88
        }
89
90
        try {
91
            $result = (array) $this->callHandler('authorize', $provider, $params);
92
        } catch (Exception $ex) {
93
            $result = array();
94
        }
95
96
        $this->hook->attach('module.oauth.authorize.after', $provider, $params, $result, $this);
97
        return $result;
98
    }
99
100
    /**
101
     * Returns an Oauth provider
102
     * @param string $id
103
     * @return array
104
     * @throws OutOfRangeException
105
     */
106
    public function getProvider($id)
107
    {
108
        $providers = $this->getProviders();
109
110
        if (empty($providers[$id])) {
111
            throw new OutOfRangeException('Unknown provider ID');
112
        }
113
114
        return $providers[$id];
115
    }
116
117
    /**
118
     * Returns an array of Oauth providers
119
     * @param array $options
120
     * @return array
121
     */
122
    public function getProviders(array $options = array())
123
    {
124
        $providers = &gplcart_static(gplcart_array_hash(array('module.oauth.providers' => $options)));
125
126
        if (isset($providers)) {
127
            return $providers;
128
        }
129
130
        $providers = array();
131
        $this->hook->attach('module.oauth.providers', $providers, $this);
132
        return $this->prepareProviders($providers, $options);
133
    }
134
135
    /**
136
     * Returns an array of URL query to redirect a user to an authorization server
137
     * @param array $provider
138
     * @param array $params
139
     * @return array
140
     * @throws OutOfRangeException
141
     */
142
    public function getQueryAuth(array $provider, array $params = array())
143
    {
144
        if (empty($provider['id'])) {
145
            throw new OutOfRangeException('Empty "id" key in the provider data');
146
        }
147
148
        $default = array(
149
            'response_type' => 'code',
150
            'redirect_uri' => $this->url->get('oauth', array(), true)
151
        );
152
153
        $query = array_merge($default, $params);
154
155
        if (!isset($query['state'])) {
156
            $query['state'] = $this->encodeState($provider['id']);
157
            $this->setState($query['state'], $provider['id']);
158
        }
159
160
        if (!isset($query['scope']) && isset($provider['scope'])) {
161
            $query['scope'] = $provider['scope'];
162
        }
163
164
        if (!isset($query['client_id']) && isset($provider['settings']['client_id'])) {
165
            $query['client_id'] = $provider['settings']['client_id'];
166
        }
167
168
        if (!empty($provider['handlers']['auth_query'])) {
169
            $query = $this->callHandler('auth_query', $provider, $query);
170
        }
171
172
        return $query;
173
    }
174
175
    /**
176
     * Returns an array of URL query to request an access token
177
     * @param array $provider
178
     * @param array $params
179
     * @return array
180
     */
181
    public function getQueryToken(array $provider, array $params = array())
182
    {
183
        $default = array(
184
            'grant_type' => 'authorization_code',
185
            'redirect_uri' => $this->url->get('oauth', array(), true)
186
        );
187
188
        $query = array_merge($default, $params);
189
190
        if (!isset($query['client_id']) && isset($provider['settings']['client_id'])) {
191
            $query['client_id'] = $provider['settings']['client_id'];
192
        }
193
194
        if (!isset($query['client_secret']) && isset($provider['settings']['client_secret'])) {
195
            $query['client_secret'] = $provider['settings']['client_secret'];
196
        }
197
198
        return $query;
199
    }
200
201
    /**
202
     * Returns the full URL to an authorization server
203
     * @param array $provider
204
     * @param array $params
205
     * @return string
206
     */
207
    public function getAuthUrl(array $provider, array $params = array())
208
    {
209
        if (empty($provider['url']['auth'])) {
210
            return '';
211
        }
212
213
        try {
214
            $query = $this->getQueryAuth($provider, $params);
215
            return $this->url->get($provider['url']['auth'], $query, true);
216
        } catch (Exception $ex) {
217
            return '';
218
        }
219
    }
220
221
    /**
222
     * Build a state code for the given provider
223
     * @param string $provider_id
224
     * @return string
225
     * @throws UnexpectedValueException
226
     */
227
    public function encodeState($provider_id)
228
    {
229
        $data = array(
230
            'id' => $provider_id,
231
            'url' => $this->url->get('', array(), true),
232
            'key' => gplcart_string_random(4), // Make resulting hash unique
233
        );
234
235
        $state = gplcart_string_encode(json_encode($data));
236
237
        if (empty($state)) {
238
            throw new UnexpectedValueException('Failed to encode the state data');
239
        }
240
241
        return $state;
242
    }
243
244
    /**
245
     * Decode the state code
246
     * @param string $state
247
     * @return array
248
     * @throws UnexpectedValueException
249
     * @throws OutOfRangeException
250
     */
251
    public function decodeState($state)
252
    {
253
        $decoded = gplcart_string_decode($state);
254
255
        if (empty($decoded)) {
256
            throw new UnexpectedValueException('Failed to decode base64 encoded string');
257
        }
258
259
        $data = $this->decodeJson($decoded);
260
261
        if (empty($data['id'])) {
262
            throw new OutOfRangeException('Empty "id" key in the decoded state data');
263
        }
264
265
        return $data;
266
    }
267
268
    /**
269
     * Save the state code in the session
270
     * @param string $state
271
     * @param string $provider_id
272
     * @return bool
273
     */
274
    public function setState($state, $provider_id)
275
    {
276
        return $this->session->set("module.oauth.state.$provider_id", $state);
277
    }
278
279
    /**
280
     * Returns a saved state data for the provider from the session
281
     * @param string $provider_id
282
     * @return string
283
     */
284
    public function getState($provider_id)
285
    {
286
        return $this->session->get("module.oauth.state.$provider_id");
287
    }
288
289
    /**
290
     * Remove a state form the session
291
     * @param null|null $provider_id
292
     * @return bool
293
     */
294
    public function unsetState($provider_id = null)
295
    {
296
        $key = 'module.oauth.state';
297
298
        if (isset($provider_id)) {
299
            $key .= ".$provider_id";
300
        }
301
302
        return $this->session->delete($key);
303
    }
304
305
    /**
306
     * Save the token data in the session
307
     * @param array $token
308
     * @param string $provider_id
309
     */
310
    public function setToken(array $token, $provider_id)
311
    {
312
        if (isset($token['expires_in'])) {
313
            $token['expires'] = GC_TIME + $token['expires_in'];
314
        }
315
316
        $this->session->set("module.oauth.token.$provider_id", $token);
317
    }
318
319
    /**
320
     * Whether a token for the given provider is valid
321
     * @param string $provider_id
322
     * @return bool
323
     */
324
    public function isValidToken($provider_id)
325
    {
326
        $token = $this->getToken($provider_id);
327
328
        return isset($token['access_token']) && isset($token['expires']) && GC_TIME < $token['expires'];
329
    }
330
331
    /**
332
     * Whether the state for the provider is valid
333
     * @param string $state
334
     * @param string $provider_id
335
     * @return bool
336
     */
337
    public function isValidState($state, $provider_id)
338
    {
339
        return gplcart_string_equals($state, $this->getState($provider_id));
340
    }
341
342
    /**
343
     * Returns a saved token data for the provider from the session
344
     * @param string $provider_id
345
     * @return array
346
     */
347
    public function getToken($provider_id)
348
    {
349
        return $this->session->get("oauth.token.$provider_id");
350
    }
351
352
    /**
353
     * Performs an HTTP request to get an access token
354
     * @param array $provider
355
     * @param array $query
356
     * @return array
357
     * @throws OutOfRangeException
358
     */
359
    public function requestToken(array $provider, array $query)
360
    {
361
        $result = null;
362
        $this->hook->attach('module.oauth.request.token.before', $provider, $query, $result, $this);
363
364
        if (isset($result)) {
365
            return (array) $result;
366
        }
367
368
        if (empty($provider['url']['token'])) {
369
            throw new OutOfRangeException('Token URL is empty in the provider data');
370
        }
371
372
        $post = array(
373
            'data' => $query,
374
            'method' => 'POST'
375
        );
376
377
        $response = $this->http->request($provider['url']['token'], $post);
378
        $result = $this->decodeJson($response['data']);
379
380
        $this->hook->attach('module.oauth.request.token.after', $provider, $query, $result, $this);
381
        return $result;
382
    }
383
384
    /**
385
     * Returns an array of requested token data
386
     * @param array $provider
387
     * @param array $params
388
     * @return array
389
     * @throws OutOfRangeException
390
     */
391
    public function exchangeToken(array $provider, array $params = array())
392
    {
393
        if (empty($provider['id'])) {
394
            throw new OutOfRangeException('Empty "id" key in the provider data');
395
        }
396
397
        if ($this->isValidToken($provider['id'])) {
398
            return $this->getToken($provider['id']);
399
        }
400
401
        if (!empty($provider['handlers']['token'])) {
402
            $token = $this->callHandler('token', $provider, $params);
403
        } else {
404
            $token = $this->requestToken($provider, $params);
405
        }
406
407
        $this->setToken($token, $provider['id']);
408
        return $token;
409
    }
410
411
    /**
412
     * Returns an array of requested token for "server-to-server" authorization
413
     * @param array $provider
414
     * @param array $jwt
415
     * @return mixed
416
     * @throws OutOfRangeException
417
     */
418
    public function exchangeTokenServer($provider, $jwt)
419
    {
420
        if (empty($provider['id'])) {
421
            throw new OutOfRangeException('Empty "id" key in the provider data');
422
        }
423
424
        if ($this->isValidToken($provider['id'])) {
425
            return $this->getToken($provider['id']);
426
        }
427
428
        if (!isset($jwt['token_url']) && isset($provider['url']['token'])) {
429
            $jwt['token_url'] = $provider['url']['token'];
430
        }
431
432
        if (!isset($jwt['scope']) && isset($provider['scope'])) {
433
            $jwt['scope'] = $provider['scope'];
434
        }
435
436
        $request = array(
437
            'assertion' => $this->encodeJwt($jwt, $provider),
438
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer'
439
        );
440
441
        $token = $this->requestToken($provider, $request);
442
        $this->setToken($token, $provider['id']);
443
        return $token;
444
    }
445
446
    /**
447
     * Encode JWT token data
448
     * @param array $data
449
     * @param array $provider
450
     * @return string
451
     * @throws OutOfRangeException
452
     */
453
    public function encodeJwt(array $data, array $provider)
454
    {
455
        if (!isset($provider['settings']['client_secret'])) {
456
            throw new OutOfRangeException('Key "client_secret" is not set in the provider settings');
457
        }
458
459
        $data += array('lifetime' => 3600);
460
        return $this->jwt->encode($data, $provider['settings']['client_secret']);
461
    }
462
463
    /**
464
     * Call a provider handler
465
     * @param string $handler_name
466
     * @param array $provider
467
     * @param array $params
468
     * @return mixed
469
     */
470
    public function callHandler($handler_name, array $provider, array $params)
471
    {
472
        $providers = $this->getProviders();
473
        return Handler::call($providers, $provider['id'], $handler_name, array($params, $provider, $this));
474
    }
475
476
    /**
477
     * Prepare an array of Oauth providers
478
     * @param array $providers
479
     * @param array $options
480
     * @return array
481
     */
482
    protected function prepareProviders(array $providers, array $options)
483
    {
484
        foreach ($providers as $provider_id => &$provider) {
485
486
            $provider['id'] = $provider_id;
487
488
            if (isset($provider['scope']) && is_array($provider['scope'])) {
489
                $provider['scope'] = implode(' ', $provider['scope']);
490
            }
491
492
            if (isset($options['type'])
493
                && isset($provider['type'])
494
                && $options['type'] !== $provider['type']) {
495
                unset($providers[$provider_id]);
496
                continue;
497
            }
498
499
            if (isset($options['status'])
500
                && isset($provider['status'])
501
                && $options['status'] != $provider['status']) {
502
                unset($providers[$provider_id]);
503
            }
504
        }
505
506
        return $providers;
507
    }
508
509
    /**
510
     * Decode JSON string
511
     * @param string $json
512
     * @return array
513
     * @throws UnexpectedValueException
514
     */
515
    protected function decodeJson($json)
516
    {
517
        $decoded = json_decode($json, true);
518
519
        if (!is_array($decoded)) {
520
            throw new UnexpectedValueException('Failed to decode JSON');
521
        }
522
523
        return $decoded;
524
    }
525
526
}
527