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(); |
|
|
|
|
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'); |
|
|
|
|
369
|
|
|
$clientSecret = $f3->get('REQUEST.client_secret'); |
|
|
|
|
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)) { |
|
|
|
|
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
|
|
|
|
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.
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.