Completed
Push — dev-master ( e1a6ef...11f3cc )
by Vijay
03:14
created

API::validateAccess()   F

Complexity

Conditions 18
Paths 483

Size

Total Lines 93
Code Lines 53

Duplication

Lines 17
Ratio 18.28 %

Importance

Changes 0
Metric Value
cc 18
eloc 53
nc 483
nop 0
dl 17
loc 93
rs 3.1829
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace FFCMS\Controllers\API;
4
5
use FFMVC\Helpers;
6
use FFCMS\{Traits, Models, Mappers};
7
8
9
/**
10
 * Api Controller Class.
11
 *
12
 * @author Vijay Mahrra <[email protected]>
13
 * @copyright Vijay Mahrra
14
 * @license GPLv3 (http://www.gnu.org/licenses/gpl-3.0.html)
15
 */
16
class API
17
{
18
    use Traits\UrlHelper,
19
        Traits\Validation,
20
        Traits\SecurityController;
21
22
    /**
23
     * response errors
24
     * 1xx: Informational - Transfer Protocol Information
25
     * 2xx: Success - Client's request successfully accepted
26
     *     - 200 OK, 201 - Created, 202 - Accepted, 204 - No Content (purposefully)
27
     * 3xx: Redirection - Client needs additional action to complete request
28
     *     - 301 - new location for resource
29
     *     - 304 - not modified
30
     * 4xx: Client Error - Client caused the problem
31
     *     - 400 - Bad request - nonspecific failure
32
     *     - 401 - unauthorised
33
     *     - 403 - forbidden
34
     *     - 404 - not found
35
     *     - 405 - method not allowed
36
     *     - 406 - not acceptable (e.g. not in correct format like json)
37
     * 5xx: Server Error - The server was responsible.
38
     *
39
     * @var array errors
40
     */
41
    protected $errors = [];
42
43
    /**
44
     * response data.
45
     *
46
     * @var array data
47
     */
48
    protected $data = [];
49
50
    /**
51
     * response params.
52
     *
53
     * @var array params
54
     */
55
    protected $params = [];
56
57
    /**
58
     * Error format required by RFC6794.
59
     *
60
     * @var type
61
     * @link https://tools.ietf.org/html/rfc6749
62
     */
63
    protected $OAuthErrorTypes = [
64
        'invalid_request' => [
65
            'code' => 'invalid_request',
66
            'description' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.',
67
            'uri' => '',
68
            'state' => '',
69
            'status' => 400
70
        ],
71
        'invalid_credentials' => [
72
            'code' => 'invalid_credentials',
73
            'description' => 'Credentials for authentication were invalid.',
74
            'uri' => '',
75
            'state' => '',
76
            'status' => 403
77
        ],
78
        'invalid_client' => [
79
            'code' => 'invalid_client',
80
            'description' => 'Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).',
81
            'uri' => '',
82
            'state' => '',
83
            'status' => 401
84
        ],
85
        'invalid_grant' => [
86
            'code' => 'invalid_grant',
87
            'description' => 'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.',
88
            'uri' => '',
89
            'state' => '',
90
            'status' => 401
91
        ],
92
        'unsupported_grant_type' => [
93
            'code' => 'unsupported_grant_type',
94
            'description' => 'The authorization grant type is not supported by the authorization server.',
95
            'uri' => '',
96
            'state' => '',
97
            'status' => 400
98
        ],
99
        'unauthorized_client' => [
100
            'code' => 'unauthorized_client',
101
            'description' => 'The client is not authorized to request an authorization code using this method.',
102
            'uri' => '',
103
            'state' => '',
104
            'status' => 401
105
        ],
106
        'access_denied' => [
107
            'code' => 'access_denied',
108
            'description' => 'The resource owner or authorization server denied the request.',
109
            'uri' => '',
110
            'state' => '',
111
            'status' => 400
112
        ],
113
        'unsupported_response_type' => [
114
            'code' => 'unsupported_response_type',
115
            'description' => 'The authorization server does not support obtaining an authorization code using this method.',
116
            'uri' => '',
117
            'state' => '',
118
            'status' => 400
119
        ],
120
        'invalid_scope' => [
121
            'code' => 'invalid_scope',
122
            'description' => 'The requested scope is invalid, unknown, or malformed.',
123
            'uri' => '',
124
            'state' => '',
125
            'status' => 400
126
        ],
127
        'server_error' => [
128
            'code' => 'server_error',
129
            'description' => 'The authorization server encountered an unexpected condition that prevented it from fulfilling the request.',
130
            'uri' => '',
131
            'state' => '',
132
            'status' => 500
133
        ],
134
        'temporarily_unavailable' => [
135
            'code' => 'temporarily_unavailable',
136
            'description' => 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.',
137
            'uri' => '',
138
            'state' => '',
139
            'status' => 400
140
        ],
141
    ];
142
143
    /**
144
     * The OAuth Error to return if an OAuthError occurs.
145
     *
146
     * @var boolean|array OAuthError
147
     */
148
    protected $OAuthError = null;
149
150
    /**
151
     * initialize
152
     */
153
    public function __construct()
154
    {
155
        $this->oAudit = Models\Audit::instance();
0 ignored issues
show
Bug introduced by
The property oAudit does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
156
        $this->params['http_status'] = 200;
157
    }
158
159
    /**
160
     * compile and send the json response.
161
     *
162
     * @param \Base $f3
163
     * @return void
164
     */
165
    public function afterRoute(\Base $f3)
166
    {
167
        $this->params['headers'] = empty($this->params['headers']) ? [] : $this->params['headers'];
168
        $this->params['headers']['Version'] = $f3->get('api.version');
169
170
        // if an OAuthError is set, return that too
171
        $data = [];
172
        if (!empty($this->OAuthError)) {
173
            $data['error'] = $this->OAuthError;
174
        }
175
176
        if (count($this->errors)) {
177
            foreach ($this->errors as $code => $message) {
178
                $data['error']['errors'][] = [
179
                    'code' => $code,
180
                    'message' => $message
181
                ];
182
            }
183
            ksort($this->errors);
184
        }
185
186
        Helpers\Response::json(array_merge($data, $this->data), $this->params);
187
    }
188
189
    /**
190
     * add to the list of errors that occured during this request.
191
     *
192
     * @param string $code        the error code
193
     * @param string $message     the error message
194
     * @param null|int    $http_status the http status code
195
     * @return void
196
     */
197
    public function failure(string $code, string $message, int $http_status = null)
198
    {
199
        $this->errors[$code] = $message;
200
201
        if (!empty($http_status)) {
202
            $this->params['http_status'] = $http_status;
203
        }
204
    }
205
206
    /**
207
     * Get OAuth Error Type.
208
     *
209
     * @param string $type
210
     *
211
     * @return array|bool error type or boolean false
212
     */
213
    protected function getOAuthErrorType(string $type)
214
    {
215
        return array_key_exists($type, $this->OAuthErrorTypes) ? $this->OAuthErrorTypes[$type] : false;
216
    }
217
218
    /**
219
     * Set the RFC-compliant OAuth Error to return.
220
     *
221
     * @param string $code  of error code from RFC
222
     * @return array|boolean the OAuth error array
223
     */
224
    public function setOAuthError(string $code)
225
    {
226
        $this->OAuthError = $this->getOAuthErrorType($code);
227
228
        // only set https status if not set anywhere else
229
        if ($this->params['http_status'] == 200) {
230
            $this->params['http_status'] = $this->OAuthError['status'];
231
        }
232
233
        return $this->OAuthError;
234
    }
235
236
    /**
237
     * Basic Authentication for email:password
238
     *
239
     * Check that the credentials match the database
240
     * Cache result for 30 seconds.
241
     *
242
     * @return bool success/failure
243
     */
244
    public function basicAuthenticateLoginPassword(): bool
245
    {
246
        $auth = new \Auth(new \DB\SQL\Mapper(\Registry::get('db'), 'users', ['email', 'password'], 30), [
247
            'id' => 'email',
248
            'pw' => 'password',
249
        ]);
250
251
        return (bool) $auth->basic(function ($pw) {
252
            return Helpers\Str::password($pw);
253
        });
254
    }
255
256
    /**
257
     * Authentication for client_id and client_secret
258
     *
259
     * Check that the credentials match a registered app
260
     * @param string $clientId the client id to check
261
     * @param string $clientSecret the client secret to check
262
     * @return bool success/failure
263
     */
264
    public function authenticateClientIdSecret(string $clientId, string $clientSecret): bool
265
    {
266
        if (empty($clientId) || empty($clientSecret)) {
267
            return false;
268
        }
269
        $oAuth2Model = Models\OAuth2::instance();
270
        $appsMapper = $oAuth2Model->getAppsMapper();
271
        $appsMapper->load(['client_id = ? AND client_secret = ?',
272
            $clientId,
273
            $clientSecret
274
        ]);
275
276
        return !empty($appsMapper->client_id);
277
    }
278
279
    /**
280
     * Basic Authentication for client_id:client_secret
281
     *
282
     * Check that the credentials match a registered app
283
     *
284
     * @return bool success/failure
285
     */
286
    public function basicAuthenticateClientIdSecret(): bool
287
    {
288
        $f3 = \Base::instance();
289
        return $this->authenticateClientIdSecret($f3->get('REQUEST.PHP_AUTH_USER'), $f3->get('REQUEST.PHP_AUTH_PW'));
290
    }
291
292
    /**
293
     * Validate the provided access token or get the bearer token from the incoming http request
294
     * do $f3->set('access_token') if OK.
295
     *
296
     * Or login using app token with HTTP Auth using one of
297
     *
298
     * email:password
299
     * email:access_token
300
     *
301
     * Or by URL query string param - ?access_token=$access_token
302
     *
303
     * Sets hive vars: user[] (mandatory), api_app[] (optional) and userScopes[]
304
     *
305
     * @return null|boolean true/false on valid access credentials
306
     */
307
    protected function validateAccess()
308
    {
309
        $this->dnsbl(); // always check if dns blacklisted
310
311
        $f3 = \Base::instance();
312
313
        $oAuth2Model = Models\OAuth2::instance();
314
        $appsMapper = $oAuth2Model->getAppsMapper();
315
        $tokensMapper = $oAuth2Model->getTokensMapper();
316
        $usersModel = Models\Users::instance();
317
        $usersMapper = $usersModel->getMapper();
318
319
        // return if forcing access to https and not https
320 View Code Duplication
        if ('http' == $f3->get('SCHEME') && !empty($f3->get('api.https'))) {
321
            $this->failure('api_connection_error', "Connection only allowed via HTTPS!", 400);
322
            $this->setOAuthError('unauthorized_client');
323
324
            return false;
325
        }
326
327
        // get token from request to set the user and app
328
        // override if anything in basic auth or client_id/secret AFTER
329
        $token = $f3->get('REQUEST.access_token');
330
        if (!empty($token)) {
331
            $tokensMapper->load(['token = ?', $token]);
332
            // token does not exist!
333
            if (null == $tokensMapper->uuid) {
334
                $this->failure('authentication_error', "The token does not exist!", 401);
335
                $this->setOAuthError('invalid_grant');
336
337
                return false;
338
            }
339
            // check token is not out-of-date
340 View Code Duplication
            if (time() > strtotime($tokensMapper->expires)) {
341
                $this->failure('authentication_error', "The token expired!", 401);
342
                $this->setOAuthError('invalid_grant');
343
344
                return false;
345
            }
346
            // if token found load the user for the token
347
            $usersModel->getUserByUUID($tokensMapper->users_uuid);
348
        }
349
350
        // login with client_id and client_secret in request
351
        if ($this->basicAuthenticateClientIdSecret()) {
352
            $usersMapper->load(['uuid = ?', $appsMapper->users_uuid]);
353
        } elseif ($this->basicAuthenticateLoginPassword()) {
354
            $usersModel->getUserByEmail($f3->get('REQUEST.PHP_AUTH_USER'));
355
        }
356
357
        // check user has api access enabled
358
        // has to have 'api' in group
359
        $scopes = empty($usersMapper->scopes) ? [] : preg_split("/[\s,]+/", $usersMapper->scopes);
360
        $f3->set('isAdmin', in_array('admin', $scopes));
361
        if (empty($token) && !in_array('api', $scopes)) {
362
            // clear authorized app as user doesn't have access
363
            $usersMapper->reset();
364
        }
365
366
        // fetch scope if available
367
        $app = $appsMapper->cast();
368
        $user = $usersMapper->cast();
369
        if (!empty($app) && !empty($user)) {
370
            $tokensMapper->load(['client_id = ? AND users_uuid = ?', $app['client_id'], $user['uuid']]);
371
        }
372
373
        // get the scopes, this might have come from the token auth
374
        $scope = $f3->get('REQUEST.scope');
375
        $scopes = empty($scope) ? [] : preg_split("/[\s,]+/", $scope);
376 View Code Duplication
        if (null !== $tokensMapper->uuid && time() > strtotime($tokensMapper->expires)) {
377
            $this->failure('authentication_error', "The token expired!", 401);
378
            $this->setOAuthError('invalid_grant');
379
380
            return false;
381
        }
382
383
        $userAuthenticated = (is_array($user) || is_array($app));
384
        if (!$userAuthenticated) {
385
            $this->failure('authentication_error', "Not possible to authenticate the request.", 400);
386
            $this->setOAuthError('invalid_credentials');
387
388
            return false;
389
        }
390
391
        $f3->mset([
392
            'uuid' => $f3->set('uuid', $usersMapper->uuid),
393
            'user' => $usersMapper->cast(),
394
            'userScopes' => $scopes,
395
            'api_app' => $appsMapper->cast()
396
        ]);
397
398
        return true;
399
    }
400
401
    /**
402
     * catch-all
403
     *
404
     * @return void
405
     */
406
    public function unknown()
407
    {
408
        $this->setOAuthError('invalid_request');
409
        $this->failure('api_connection_error', 'Unknown API Request', 400);
410
    }
411
412
}
413