Completed
Push — dev-master ( dbed35...385f04 )
by Vijay
04:36
created

API::setOAuthError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
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\Logger,
19
        Traits\Audit,
20
        Traits\UrlHelper,
21
        Traits\Validation,
22
        Traits\SecurityController;
23
24
    /**
25
     * version.
26
     *
27
     * @var version
28
     */
29
    protected $version;
30
31
    /**
32
     * response errors
33
     * 1xx: Informational - Transfer Protocol Information
34
     * 2xx: Success - Client's request successfully accepted
35
     *     - 200 OK, 201 - Created, 202 - Accepted, 204 - No Content (purposefully)
36
     * 3xx: Redirection - Client needs additional action to complete request
37
     *     - 301 - new location for resource
38
     *     - 304 - not modified
39
     * 4xx: Client Error - Client caused the problem
40
     *     - 400 - Bad request - nonspecific failure
41
     *     - 401 - unauthorised
42
     *     - 403 - forbidden
43
     *     - 404 - not found
44
     *     - 405 - method not allowed
45
     *     - 406 - not acceptable (e.g. not in correct format like json)
46
     * 5xx: Server Error - The server was responsible.
47
     *
48
     * @var array errors
49
     */
50
    protected $errors = [];
51
52
    /**
53
     * response data.
54
     *
55
     * @var array data
56
     */
57
    protected $data = [];
58
59
    /**
60
     * response params.
61
     *
62
     * @var array params
63
     */
64
    protected $params = [];
65
66
    /**
67
     * response helper object.
68
     *
69
     * @var \FFMVC\Helpers\Response response
70
     */
71
    protected $oResponse;
72
73
    /**
74
     * Error format required by RFC6794.
75
     *
76
     * @var type
77
     * @link https://tools.ietf.org/html/rfc6749
78
     */
79
    protected $OAuthErrorTypes = [
80
        'invalid_request' => [
81
            'code' => 'invalid_request',
82
            'description' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.',
83
            'uri' => '',
84
            'state' => '',
85
            'status' => 400
86
        ],
87
        'invalid_credentials' => [
88
            'code' => 'invalid_credentials',
89
            'description' => 'Credentials for authentication were invalid.',
90
            'uri' => '',
91
            'state' => '',
92
            'status' => 403
93
        ],
94
        'invalid_client' => [
95
            'code' => 'invalid_client',
96
            'description' => 'Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).',
97
            'uri' => '',
98
            'state' => '',
99
            'status' => 401
100
        ],
101
        'invalid_grant' => [
102
            'code' => 'invalid_grant',
103
            '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.',
104
            'uri' => '',
105
            'state' => '',
106
            'status' => 401
107
        ],
108
        'unsupported_grant_type' => [
109
            'code' => 'unsupported_grant_type',
110
            'description' => 'The authorization grant type is not supported by the authorization server.',
111
            'uri' => '',
112
            'state' => '',
113
            'status' => 400
114
        ],
115
        'unauthorized_client' => [
116
            'code' => 'unauthorized_client',
117
            'description' => 'The client is not authorized to request an authorization code using this method.',
118
            'uri' => '',
119
            'state' => '',
120
            'status' => 401
121
        ],
122
        'access_denied' => [
123
            'code' => 'access_denied',
124
            'description' => 'The resource owner or authorization server denied the request.',
125
            'uri' => '',
126
            'state' => '',
127
            'status' => 400
128
        ],
129
        'unsupported_response_type' => [
130
            'code' => 'unsupported_response_type',
131
            'description' => 'The authorization server does not support obtaining an authorization code using this method.',
132
            'uri' => '',
133
            'state' => '',
134
            'status' => 400
135
        ],
136
        'invalid_scope' => [
137
            'code' => 'invalid_scope',
138
            'description' => 'The requested scope is invalid, unknown, or malformed.',
139
            'uri' => '',
140
            'state' => '',
141
            'status' => 400
142
        ],
143
        'server_error' => [
144
            'code' => 'server_error',
145
            'description' => 'The authorization server encountered an unexpected condition that prevented it from fulfilling the request.',
146
            'uri' => '',
147
            'state' => '',
148
            'status' => 500
149
        ],
150
        'temporarily_unavailable' => [
151
            'code' => 'temporarily_unavailable',
152
            'description' => 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.',
153
            'uri' => '',
154
            'state' => '',
155
            'status' => 400
156
        ],
157
    ];
158
159
    /**
160
     * The OAuth Error to return if an OAuthError occurs.
161
     *
162
     * @var boolean|array OAuthError
163
     */
164
    protected $OAuthError = null;
165
166
    /**
167
     * initialize
168
     *
169
     * @param \Base $f3
170
     */
171
    public function __construct(\Base $f3)
172
    {
173
        $f3 = \Base::instance();
0 ignored issues
show
Unused Code introduced by
$f3 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
174
        $this->oAudit = Models\Audit::instance();
175
        $this->params['http_status'] = 200;
176
    }
177
178
    /**
179
     * compile and send the json response.
180
     *
181
     * @param \Base $f3
182
     * @return void
183
     */
184
    public function afterRoute(\Base $f3)
185
    {
186
        $this->params['headers'] = empty($this->params['headers']) ? [] : $this->params['headers'];
187
        $this->params['headers']['Version'] = $f3->get('api.version');
188
189
        // if an OAuthError is set, return that too
190
        $data = [];
191
        if (!empty($this->OAuthError)) {
192
            $data['error'] = $this->OAuthError;
193
        }
194
195
        if (count($this->errors)) {
196
            foreach ($this->errors as $code => $message) {
197
                $data['error']['errors'][] = [
198
                    'code' => $code,
199
                    'message' => $message
200
                ];
201
            }
202
            ksort($this->errors);
203
        }
204
205
        Helpers\Response::json(array_merge($data, $this->data), $this->params);
206
    }
207
208
    /**
209
     * add to the list of errors that occured during this request.
210
     *
211
     * @param string $code        the error code
212
     * @param string $message     the error message
213
     * @param null|int    $http_status the http status code
214
     * @return void
215
     */
216
    public function failure(string $code, string $message, int $http_status = null)
217
    {
218
        $this->errors[$code] = $message;
219
220
        if (!empty($http_status)) {
221
            $this->params['http_status'] = $http_status;
222
        }
223
    }
224
225
    /**
226
     * Get OAuth Error Type.
227
     *
228
     * @param string $type
229
     *
230
     * @return array|bool error type or boolean false
231
     */
232
    protected function getOAuthErrorType(string $type)
233
    {
234
        return array_key_exists($type, $this->OAuthErrorTypes) ? $this->OAuthErrorTypes[$type] : false;
235
    }
236
237
    /**
238
     * Set the RFC-compliant OAuth Error to return.
239
     *
240
     * @param string $code  of error code from RFC
241
     * @return array|boolean the OAuth error array
242
     */
243
    public function setOAuthError(string $code)
244
    {
245
        $this->OAuthError = $this->getOAuthErrorType($code);
246
247
        // only set https status if not set anywhere else
248
        if ($this->params['http_status'] == 200) {
249
            $this->params['http_status'] = $this->OAuthError['status'];
250
        }
251
252
        return $this->OAuthError;
253
    }
254
255
    /**
256
     * Basic Authentication for email:password
257
     *
258
     * Check that the credentials match the database
259
     * Cache result for 30 seconds.
260
     *
261
     * @return bool success/failure
262
     */
263
    public function basicAuthenticateLoginPassword(): bool
264
    {
265
        $auth = new \Auth(new \DB\SQL\Mapper(\Registry::get('db'), 'users', ['email', 'password'], 30), [
266
            'id' => 'email',
267
            'pw' => 'password',
268
        ]);
269
270
        return (bool) $auth->basic(function ($pw) {
271
            return Helpers\Str::password($pw);
272
        });
273
    }
274
275
    /**
276
     * Authentication for client_id and client_secret
277
     *
278
     * Check that the credentials match a registered app
279
     * @param string $clientId the client id to check
280
     * @param string $clientSecret the client secret to check
281
     * @return bool success/failure
282
     */
283
    public function authenticateClientIdSecret(string $clientId, string $clientSecret): bool
284
    {
285
        if (empty($clientId) || empty($clientSecret)) {
286
            return false;
287
        }
288
        $oAuth2Model = Models\OAuth2::instance();
289
        $appsMapper = $oAuth2Model->getAppsMapper();
290
        $appsMapper->load(['client_id = ? AND client_secret = ?',
291
            $clientId,
292
            $clientSecret
293
        ]);
294
295
        return !empty($appsMapper->client_id);
296
    }
297
298
    /**
299
     * Basic Authentication for client_id:client_secret
300
     *
301
     * Check that the credentials match a registered app
302
     *
303
     * @return bool success/failure
304
     */
305
    public function basicAuthenticateClientIdSecret(): bool
306
    {
307
        $f3 = \Base::instance();
308
        return $this->authenticateClientIdSecret($f3->get('REQUEST.PHP_AUTH_USER'), $f3->get('REQUEST.PHP_AUTH_PW'));
309
    }
310
311
    /**
312
     * Validate the provided access token or get the bearer token from the incoming http request
313
     * do $f3->set('access_token') if OK.
314
     *
315
     * Or login using app token with HTTP Auth using one of
316
     *
317
     * email:password
318
     * email:access_token
319
     *
320
     * Or by URL query string param - ?access_token=$access_token
321
     *
322
     * Sets hive vars: user[] (mandatory), api_app[] (optional) and userScopes[]
323
     *
324
     * @return null|boolean true/false on valid access credentials
325
     */
326
    protected function validateAccess()
327
    {
328
        $this->dnsbl(); // always check if dns blacklisted
329
330
        $f3 = \Base::instance();
331
332
        // return if forcing access to https and not https
333 View Code Duplication
        if ('http' == $f3->get('SCHEME') && !empty($f3->get('api.https'))) {
334
            $this->failure('api_connection_error', "Connection only allowed via HTTPS!", 400);
335
            $this->setOAuthError('unauthorized_client');
336
337
            return false;
338
        }
339
340
        $oAuth2Model = Models\OAuth2::instance();
341
        $tokensMapper = $oAuth2Model->getTokensMapper();
342
        $usersModel = Models\Users::instance();
343
344
        // get token from request to set the user and app
345
        // override if anything in basic auth or client_id/secret AFTER
346
        $token = $f3->get('REQUEST.access_token');
347
        if (!empty($token)) {
348
            $tokensMapper->load(['token = ?', $token]);
349
            // token does not exist!
350
            if (null == $tokensMapper->uuid) {
351
                $this->failure('authentication_error', "The token does not exist!", 401);
352
                $this->setOAuthError('invalid_grant');
353
354
                return false;
355
            }
356
            // check token is not out-of-date
357 View Code Duplication
            if (time() > strtotime($tokensMapper->expires)) {
358
                $this->failure('authentication_error', "The token expired!", 401);
359
                $this->setOAuthError('invalid_grant');
360
361
                return false;
362
            }
363
            // if token found load the user for the token
364
            $usersModel->getUserByUUID($tokensMapper->users_uuid);
365
        }
366
367
        // login with client_id and client_secret in request
368
        $clientId = $f3->get('REQUEST.client_id');
0 ignored issues
show
Unused Code introduced by
$clientId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
369
        $clientSecret = $f3->get('REQUEST.client_secret');
0 ignored issues
show
Unused Code introduced by
$clientSecret is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
370
        if ($this->basicAuthenticateClientIdSecret()) {
371
            $appLogin = true; // client_id:client_secret
372
        } elseif ($this->basicAuthenticateLoginPassword()) {
373
            // login with basic auth of email:password
374
            $email = $f3->get('REQUEST.PHP_AUTH_USER');
375
            $usersModel->getUserByEmail($email);
376
        }
377
378
        // login with app credentials: client_id/client_secret?
379
        // if so fetch app and user information
380
        $usersMapper = $usersModel->getMapper();
381
        $appsMapper = $oAuth2Model->getAppsMapper();
382
        if (!empty($appLogin)) {
383
            $usersMapper->load(['uuid = ?', $appsMapper->users_uuid]);
384
        }
385
386
        // check user has api access enabled
387
        // has to have 'api' in group
388
        $scopes = empty($usersMapper->scopes) ? [] : preg_split("/[\s,]+/", $usersMapper->scopes);
389
        $f3->set('isAdmin', in_array('admin', $scopes));
390
        if (empty($token) && !in_array('api', $scopes)) {
391
            // clear authorized app as user doesn't have access
392
            $usersMapper->reset();
393
        }
394
395
        // fetch scope if available
396
        $app = $appsMapper->cast();
397
        $user = $usersMapper->cast();
398
        if (!empty($app) && !empty($user)) {
399
            $tokensMapper->load(['client_id = ? AND users_uuid = ?', $app['client_id'], $user['uuid']]);
400
        }
401
402
        // get the scopes, this might have come from the token auth
403
        $scope = $f3->get('REQUEST.scope');
404
        $scopes = empty($scope) ? [] : preg_split("/[\s,]+/", $scope);
405
        if (!empty($tokensMapper->users_uuid) && !$appLogin && time() > strtotime($tokensMapper->expires)) {
0 ignored issues
show
Bug introduced by
The variable $appLogin does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
406
            $this->failure('authentication_error', "The token expired!", 401);
407
            $this->setOAuthError('invalid_grant');
408
409
            return false;
410
        }
411
412
        $userAuthenticated = (is_array($user) || is_array($app));
413
        if (!$userAuthenticated) {
414
            $this->failure('authentication_error', "Not possible to authenticate the request.", 400);
415
            $this->setOAuthError('invalid_credentials');
416
417
            return false;
418
        }
419
420
        $f3->mset([
421
            'uuid' => $f3->set('uuid', $usersMapper->uuid),
422
            'user' => $usersMapper->cast(),
423
            'userScopes' => $scopes,
424
            'api_app' => $appsMapper->cast()
425
        ]);
426
427
        return true;
428
    }
429
430
    /**
431
     * catch-all
432
     *
433
     * @return void
434
     */
435
    public function unknown()
436
    {
437
        $this->setOAuthError('invalid_request');
438
        $this->failure('api_connection_error', 'Unknown API Request', 400);
439
    }
440
441
}
442