API::validateAccess()   F
last analyzed

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