1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace App\Controllers\API; |
4
|
|
|
|
5
|
|
|
use FFMVC\Helpers; |
6
|
|
|
|
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Api Controller Class. |
10
|
|
|
* |
11
|
|
|
* @author Vijay Mahrra <[email protected]> |
12
|
|
|
* @copyright Vijay Mahrra |
13
|
|
|
* @license GPLv3 (http://www.gnu.org/licenses/gpl-3.0.html) |
14
|
|
|
*/ |
15
|
|
|
class API |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* response errors |
19
|
|
|
* 1xx: Informational - Transfer Protocol Information |
20
|
|
|
* 2xx: Success - Client's request successfully accepted |
21
|
|
|
* - 200 OK, 201 - Created, 202 - Accepted, 204 - No Content (purposefully) |
22
|
|
|
* 3xx: Redirection - Client needs additional action to complete request |
23
|
|
|
* - 301 - new location for resource |
24
|
|
|
* - 304 - not modified |
25
|
|
|
* 4xx: Client Error - Client caused the problem |
26
|
|
|
* - 400 - Bad request - nonspecific failure |
27
|
|
|
* - 401 - unauthorised |
28
|
|
|
* - 403 - forbidden |
29
|
|
|
* - 404 - not found |
30
|
|
|
* - 405 - method not allowed |
31
|
|
|
* - 406 - not acceptable (e.g. not in correct format like json) |
32
|
|
|
* 5xx: Server Error - The server was responsible. |
33
|
|
|
* |
34
|
|
|
* @var array errors |
35
|
|
|
*/ |
36
|
|
|
protected $errors = []; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* response data. |
40
|
|
|
* |
41
|
|
|
* @var array data |
42
|
|
|
*/ |
43
|
|
|
protected $data = []; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* response params. |
47
|
|
|
* |
48
|
|
|
* @var array params |
49
|
|
|
*/ |
50
|
|
|
protected $params = []; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Error format required by RFC6794. |
54
|
|
|
* |
55
|
|
|
* @var type |
56
|
|
|
* @link https://tools.ietf.org/html/rfc6749 |
57
|
|
|
*/ |
58
|
|
|
protected $OAuthErrorTypes = [ |
59
|
|
|
'invalid_request' => [ |
60
|
|
|
'code' => 'invalid_request', |
61
|
|
|
'description' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.', |
62
|
|
|
'uri' => '', |
63
|
|
|
'state' => '', |
64
|
|
|
'status' => 400 |
65
|
|
|
], |
66
|
|
|
'invalid_credentials' => [ |
67
|
|
|
'code' => 'invalid_credentials', |
68
|
|
|
'description' => 'Credentials for authentication were invalid.', |
69
|
|
|
'uri' => '', |
70
|
|
|
'state' => '', |
71
|
|
|
'status' => 403 |
72
|
|
|
], |
73
|
|
|
'invalid_client' => [ |
74
|
|
|
'code' => 'invalid_client', |
75
|
|
|
'description' => 'Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).', |
76
|
|
|
'uri' => '', |
77
|
|
|
'state' => '', |
78
|
|
|
'status' => 401 |
79
|
|
|
], |
80
|
|
|
'invalid_grant' => [ |
81
|
|
|
'code' => 'invalid_grant', |
82
|
|
|
'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.', |
83
|
|
|
'uri' => '', |
84
|
|
|
'state' => '', |
85
|
|
|
'status' => 401 |
86
|
|
|
], |
87
|
|
|
'unsupported_grant_type' => [ |
88
|
|
|
'code' => 'unsupported_grant_type', |
89
|
|
|
'description' => 'The authorization grant type is not supported by the authorization server.', |
90
|
|
|
'uri' => '', |
91
|
|
|
'state' => '', |
92
|
|
|
'status' => 400 |
93
|
|
|
], |
94
|
|
|
'unauthorized_client' => [ |
95
|
|
|
'code' => 'unauthorized_client', |
96
|
|
|
'description' => 'The client is not authorized to request an authorization code using this method.', |
97
|
|
|
'uri' => '', |
98
|
|
|
'state' => '', |
99
|
|
|
'status' => 401 |
100
|
|
|
], |
101
|
|
|
'access_denied' => [ |
102
|
|
|
'code' => 'access_denied', |
103
|
|
|
'description' => 'The resource owner or authorization server denied the request.', |
104
|
|
|
'uri' => '', |
105
|
|
|
'state' => '', |
106
|
|
|
'status' => 400 |
107
|
|
|
], |
108
|
|
|
'unsupported_response_type' => [ |
109
|
|
|
'code' => 'unsupported_response_type', |
110
|
|
|
'description' => 'The authorization server does not support obtaining an authorization code using this method.', |
111
|
|
|
'uri' => '', |
112
|
|
|
'state' => '', |
113
|
|
|
'status' => 400 |
114
|
|
|
], |
115
|
|
|
'invalid_scope' => [ |
116
|
|
|
'code' => 'invalid_scope', |
117
|
|
|
'description' => 'The requested scope is invalid, unknown, or malformed.', |
118
|
|
|
'uri' => '', |
119
|
|
|
'state' => '', |
120
|
|
|
'status' => 400 |
121
|
|
|
], |
122
|
|
|
'server_error' => [ |
123
|
|
|
'code' => 'server_error', |
124
|
|
|
'description' => 'The authorization server encountered an unexpected condition that prevented it from fulfilling the request.', |
125
|
|
|
'uri' => '', |
126
|
|
|
'state' => '', |
127
|
|
|
'status' => 500 |
128
|
|
|
], |
129
|
|
|
'temporarily_unavailable' => [ |
130
|
|
|
'code' => 'temporarily_unavailable', |
131
|
|
|
'description' => 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.', |
132
|
|
|
'uri' => '', |
133
|
|
|
'state' => '', |
134
|
|
|
'status' => 400 |
135
|
|
|
], |
136
|
|
|
]; |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* The OAuth Error to return if an OAuthError occurs. |
140
|
|
|
* |
141
|
|
|
* @var boolean|array OAuthError |
142
|
|
|
*/ |
143
|
|
|
protected $OAuthError = null; |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* initialize |
147
|
|
|
*/ |
148
|
|
|
public function __construct() |
149
|
|
|
{ |
150
|
|
|
$this->params['http_status'] = 200; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* compile and send the json response. |
155
|
|
|
* |
156
|
|
|
* @param \Base $f3 |
157
|
|
|
* @return void |
158
|
|
|
*/ |
159
|
|
|
public function afterRoute(\Base $f3) |
160
|
|
|
{ |
161
|
|
|
$this->params['headers'] = empty($this->params['headers']) ? [] : $this->params['headers']; |
162
|
|
|
$this->params['headers']['Version'] = $f3->get('api.version'); |
163
|
|
|
|
164
|
|
|
// if an OAuthError is set, return that too |
165
|
|
|
$data = []; |
166
|
|
|
if (!empty($this->OAuthError)) { |
167
|
|
|
$data['error'] = $this->OAuthError; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
if (count($this->errors)) { |
171
|
|
|
foreach ($this->errors as $code => $message) { |
172
|
|
|
$data['error']['errors'][] = [ |
173
|
|
|
'code' => $code, |
174
|
|
|
'message' => $message |
175
|
|
|
]; |
176
|
|
|
} |
177
|
|
|
ksort($this->errors); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
Helpers\Response::json(array_merge($data, $this->data), $this->params); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* add to the list of errors that occured during this request. |
185
|
|
|
* |
186
|
|
|
* @param string $code the error code |
187
|
|
|
* @param string $message the error message |
188
|
|
|
* @param null|int $http_status the http status code |
189
|
|
|
* @return void |
190
|
|
|
*/ |
191
|
|
|
public function failure(string $code, string $message, int $http_status = null) |
192
|
|
|
{ |
193
|
|
|
$this->errors[$code] = $message; |
194
|
|
|
|
195
|
|
|
if (!empty($http_status)) { |
196
|
|
|
$this->params['http_status'] = $http_status; |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Get OAuth Error Type. |
202
|
|
|
* |
203
|
|
|
* @param string $type |
204
|
|
|
* |
205
|
|
|
* @return array|bool error type or boolean false |
206
|
|
|
*/ |
207
|
|
|
protected function getOAuthErrorType(string $type) |
208
|
|
|
{ |
209
|
|
|
return array_key_exists($type, $this->OAuthErrorTypes) ? $this->OAuthErrorTypes[$type] : false; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Set the RFC-compliant OAuth Error to return. |
214
|
|
|
* |
215
|
|
|
* @param string $code of error code from RFC |
216
|
|
|
* @return array|boolean the OAuth error array |
217
|
|
|
*/ |
218
|
|
|
public function setOAuthError(string $code) |
219
|
|
|
{ |
220
|
|
|
$this->OAuthError = $this->getOAuthErrorType($code); |
221
|
|
|
|
222
|
|
|
// only set https status if not set anywhere else |
223
|
|
|
if ($this->params['http_status'] == 200) { |
224
|
|
|
$this->params['http_status'] = $this->OAuthError['status']; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
return $this->OAuthError; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Basic Authentication for email:password |
232
|
|
|
* |
233
|
|
|
* Check that the credentials match the database |
234
|
|
|
* Cache result for 30 seconds. |
235
|
|
|
* |
236
|
|
|
* @return bool success/failure |
237
|
|
|
*/ |
238
|
|
|
public function basicAuthenticateLoginPassword(): bool |
239
|
|
|
{ |
240
|
|
|
$auth = new \Auth(new \DB\SQL\Mapper(\Registry::get('db'), 'users', ['email', 'password'], 30), [ |
241
|
|
|
'id' => 'email', |
242
|
|
|
'pw' => 'password', |
243
|
|
|
]); |
244
|
|
|
|
245
|
|
|
return (bool) $auth->basic(function ($pw) { |
246
|
|
|
return Helpers\Str::password($pw); |
247
|
|
|
}); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Authentication for client_id and client_secret |
252
|
|
|
* |
253
|
|
|
* Check that the credentials match a registered app |
254
|
|
|
* @param string $clientId the client id to check |
255
|
|
|
* @param string $clientSecret the client secret to check |
256
|
|
|
* @return bool success/failure |
257
|
|
|
*/ |
258
|
|
|
public function authenticateClientIdSecret(string $clientId, string $clientSecret): bool |
259
|
|
|
{ |
260
|
|
|
if (empty($clientId) || empty($clientSecret)) { |
261
|
|
|
return false; |
262
|
|
|
} |
263
|
|
|
// validate client_id/secret |
264
|
|
|
return false; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Basic Authentication for client_id:client_secret |
269
|
|
|
* |
270
|
|
|
* Check that the credentials match a registered app |
271
|
|
|
* |
272
|
|
|
* @return bool success/failure |
273
|
|
|
*/ |
274
|
|
|
public function basicAuthenticateClientIdSecret(): bool |
275
|
|
|
{ |
276
|
|
|
$f3 = \Base::instance(); |
277
|
|
|
return $this->authenticateClientIdSecret($f3->get('REQUEST.PHP_AUTH_USER'), $f3->get('REQUEST.PHP_AUTH_PW')); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Validate the provided access token or get the bearer token from the incoming http request |
282
|
|
|
* do $f3->set('access_token') if OK. |
283
|
|
|
* |
284
|
|
|
* Or login using app token with HTTP Auth using one of |
285
|
|
|
* |
286
|
|
|
* email:password |
287
|
|
|
* email:access_token |
288
|
|
|
* |
289
|
|
|
* Or by URL query string param - ?access_token=$access_token |
290
|
|
|
* |
291
|
|
|
* Sets hive vars: user[] (mandatory), api_app[] (optional) and userScopes[] |
292
|
|
|
* |
293
|
|
|
* @return null|boolean true/false on valid access credentials |
294
|
|
|
*/ |
295
|
|
|
protected function validateAccess() |
296
|
|
|
{ |
297
|
|
|
$f3 = \Base::instance(); |
298
|
|
|
|
299
|
|
|
// return if forcing access to https and not https |
300
|
|
|
if ('http' == $f3->get('SCHEME') && !empty($f3->get('api.https'))) { |
301
|
|
|
$this->failure('api_connection_error', "Connection only allowed via HTTPS!", 400); |
302
|
|
|
$this->setOAuthError('unauthorized_client'); |
303
|
|
|
|
304
|
|
|
return false; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
// get token from request to set the user and app |
308
|
|
|
// override if anything in basic auth or client_id/secret AFTER |
309
|
|
|
$token = $f3->get('REQUEST.access_token'); |
310
|
|
|
if (!empty($token)) { |
311
|
|
|
// token does not exist! |
312
|
|
|
$tokenExists = null; // perform a real check here against your db! |
313
|
|
View Code Duplication |
if (null == $tokenExists) { |
314
|
|
|
$this->failure('authentication_error', "The token does not exist!", 401); |
315
|
|
|
$this->setOAuthError('invalid_grant'); |
316
|
|
|
|
317
|
|
|
return false; |
318
|
|
|
} |
319
|
|
|
// check token is not out-of-date |
320
|
|
|
$expires = 0; // get the expiry from your db! |
321
|
|
View Code Duplication |
if (time() > $expires) { |
322
|
|
|
$this->failure('authentication_error', "The token expired!", 401); |
323
|
|
|
$this->setOAuthError('invalid_grant'); |
324
|
|
|
|
325
|
|
|
return false; |
326
|
|
|
} |
327
|
|
|
// if token found load the user for the token from your db! |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
// login with client_id and client_secret in request OR login/password using basic auth |
331
|
|
|
if ($f3->get('REQUEST.PHP_AUTH_USER')) { |
332
|
|
|
if ($this->basicAuthenticateClientIdSecret()) { |
333
|
|
|
} elseif ($this->basicAuthenticateLoginPassword()) { |
334
|
|
|
} |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
$userAuthenticated = false; // do a proper check here based on the above |
338
|
|
|
if (!$userAuthenticated) { |
339
|
|
|
$this->failure('authentication_error', "Not possible to authenticate the request.", 400); |
340
|
|
|
$this->setOAuthError('invalid_credentials'); |
341
|
|
|
|
342
|
|
|
return false; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
return true; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* catch-all |
350
|
|
|
* |
351
|
|
|
* @return void |
352
|
|
|
*/ |
353
|
|
|
public function unknown() |
354
|
|
|
{ |
355
|
|
|
$this->setOAuthError('invalid_request'); |
356
|
|
|
$this->failure('api_connection_error', 'Unknown API Request', 400); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
} |
360
|
|
|
|