1
|
|
|
<?php namespace Myth\Api\Auth; |
2
|
|
|
/** |
3
|
|
|
* Sprint |
4
|
|
|
* |
5
|
|
|
* A set of power tools to enhance the CodeIgniter framework and provide consistent workflow. |
6
|
|
|
* |
7
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy |
8
|
|
|
* of this software and associated documentation files (the "Software"), to deal |
9
|
|
|
* in the Software without restriction, including without limitation the rights |
10
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
11
|
|
|
* copies of the Software, and to permit persons to whom the Software is |
12
|
|
|
* furnished to do so, subject to the following conditions: |
13
|
|
|
* |
14
|
|
|
* The above copyright notice and this permission notice shall be included in |
15
|
|
|
* all copies or substantial portions of the Software. |
16
|
|
|
* |
17
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
18
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
19
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
20
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
21
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
22
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
23
|
|
|
* THE SOFTWARE. |
24
|
|
|
* |
25
|
|
|
* @package Sprint |
26
|
|
|
* @author Lonnie Ezell |
27
|
|
|
* @copyright Copyright 2014-2015, New Myth Media, LLC (http://newmythmedia.com) |
28
|
|
|
* @license http://opensource.org/licenses/MIT (MIT) |
29
|
|
|
* @link http://sprintphp.com |
30
|
|
|
* @since Version 1.0 |
31
|
|
|
*/ |
32
|
|
|
|
33
|
|
|
use Myth\Auth\LocalAuthentication; |
34
|
|
|
use Myth\Events; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Class APIAuthentication |
38
|
|
|
* |
39
|
|
|
* @package Myth\Api\Auth |
40
|
|
|
*/ |
41
|
|
|
class APIAuthentication extends LocalAuthentication { |
42
|
|
|
|
43
|
|
|
protected $logged_in = false; |
44
|
|
|
|
45
|
|
|
protected $realm = 'WallyWorld'; |
46
|
|
|
|
47
|
|
|
protected $email = null; |
48
|
|
|
|
49
|
|
|
//-------------------------------------------------------------------- |
50
|
|
|
|
51
|
|
|
public function __construct($ci=null) |
52
|
|
|
{ |
53
|
|
|
parent::__construct($ci); |
54
|
|
|
|
55
|
|
|
$this->ci->config->load('api'); |
56
|
|
|
$this->ci->lang->load('api'); |
57
|
|
|
|
58
|
|
|
// Has the IP address been blacklisted? |
59
|
|
|
if (config_item('auth.ip_blacklist_enabled')) |
60
|
|
|
{ |
61
|
|
|
$this->checkIPBlacklist(); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
// Do we need to do whitelisting? |
65
|
|
|
if (config_item('auth.ip_whitelist_enabled')) |
66
|
|
|
{ |
67
|
|
|
$this->checkIPWhitelist(); |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
//-------------------------------------------------------------------- |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Sets the realm used by the authentication. The system truly only |
75
|
|
|
* supports a single realm across the entire application, but this |
76
|
|
|
* allows it to be set by the controller. |
77
|
|
|
* |
78
|
|
|
* @param $realm |
79
|
|
|
* |
80
|
|
|
* @return $this |
81
|
|
|
*/ |
82
|
|
|
public function setRealm($realm) |
83
|
|
|
{ |
84
|
|
|
$this->realm = $realm; |
85
|
|
|
return $this; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
//-------------------------------------------------------------------- |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Checks to see if someone is authorized via HTTP Basic Authentication. |
92
|
|
|
* |
93
|
|
|
* @return bool |
94
|
|
|
*/ |
95
|
|
|
public function tryBasicAuthentication() |
96
|
|
|
{ |
97
|
|
|
$username = null; |
98
|
|
|
$password = null; |
99
|
|
|
|
100
|
|
|
// mod_php |
101
|
|
|
if ($this->ci->input->server('PHP_AUTH_USER')) { |
102
|
|
|
$username = $this->ci->input->server('PHP_AUTH_USER'); |
103
|
|
|
$password = $this->ci->input->server('PHP_AUTH_PW'); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
// most other servers |
107
|
|
|
elseif ($this->ci->input->server('HTTP_AUTHENTICATION')) { |
108
|
|
|
if (strpos(strtolower($this->ci->input->server('HTTP_AUTHENTICATION')), 'basic') === 0) { |
109
|
|
|
list($username, $password) = explode(':', base64_decode(substr($this->ci->input->server('HTTP_AUTHORIZATION'), 6))); |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
// If credentials weren't provided, we can't do anything |
114
|
|
|
// so request authorization by the client. |
115
|
|
|
if (empty($username) || empty($password)) |
116
|
|
|
{ |
117
|
|
|
$this->ci->output->set_header('WWW-Authenticate: Basic realm="'. config_item('api.realm') .'"'); |
118
|
|
|
return false; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
$data = [ |
122
|
|
|
config_item('api.auth_field') => $username, |
123
|
|
|
'password' => $password |
124
|
|
|
]; |
125
|
|
|
|
126
|
|
|
// Set email for later throttling check |
127
|
|
|
if (config_item('api.auth_field') === 'email') |
128
|
|
|
{ |
129
|
|
|
$this->email = $username; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
$user = $this->validate($data, true); |
133
|
|
|
|
134
|
|
|
$this->user = $user; |
135
|
|
|
|
136
|
|
|
return $user; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
//-------------------------------------------------------------------- |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Checks to see if someone is authorized via HTTP Digest Authentication. |
143
|
|
|
* |
144
|
|
|
* NOTE: This requires that a new field, 'digest_key', be added to the user's |
145
|
|
|
* table and, during new user creation, or password reset, that the digest_key |
146
|
|
|
* be calculated as md5({username}:{realm}:{password}) |
147
|
|
|
* |
148
|
|
|
* References: |
149
|
|
|
* - http://www.faqs.org/rfcs/rfc2617.html |
150
|
|
|
* - http://www.sitepoint.com/understanding-http-digest-access-authentication/ |
151
|
|
|
*/ |
152
|
|
|
public function tryDigestAuthentication() |
153
|
|
|
{ |
154
|
|
|
$digest_string = ''; |
155
|
|
|
|
156
|
|
|
// We need to test which server authentication variable to use |
157
|
|
|
// because the PHP ISAPI module in IIS acts different from CGI |
158
|
|
|
if ($this->ci->input->server('PHP_AUTH_DIGEST')) |
159
|
|
|
{ |
160
|
|
|
$digest_string = $this->ci->input->server('PHP_AUTH_DIGEST'); |
161
|
|
|
} |
162
|
|
|
elseif ($this->ci->input->server('HTTP_AUTHORIZATION')) |
163
|
|
|
{ |
164
|
|
|
$digest_string = $this->ci->input->server('HTTP_AUTHORIZATION'); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
$nonce = md5(uniqid()); |
168
|
|
|
$opaque = md5(uniqid()); |
169
|
|
|
|
170
|
|
|
// No digest string? Then you're done. Go home. |
171
|
|
|
if (empty($digest_string)) |
172
|
|
|
{ |
173
|
|
|
$this->ci->output->set_header( sprintf('WWW-Authenticate: Digest realm="%s", nonce="%s", opaque="%s"', config_item('api.realm'), $nonce, $opaque) ); |
174
|
|
|
return false; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
// Grab the parts from the digest string. |
178
|
|
|
// They will be provided as an array of the parts: username, nonce, uri, nc, cnonce, qop, response |
179
|
|
|
$matches = []; |
180
|
|
|
preg_match_all('@(username|nonce|uri|nc|cnonce|qop|response)=[\'"]?([^\'",]+)@', $digest_string, $matches); |
181
|
|
|
$digest = (empty($matches[1]) || empty($matches[2])) ? array() : array_combine($matches[1], $matches[2]); |
182
|
|
|
|
183
|
|
|
if (! array_key_exists('username', $digest)) |
184
|
|
|
{ |
185
|
|
|
$this->ci->output->set_header( sprintf('WWW-Authenticate: Digest realm="%s", nonce="%s", opaque="%s"', config_item('api.realm'), $nonce, $opaque) ); |
186
|
|
|
return false; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
// Set email for later throttling check |
190
|
|
|
if (config_item('api.auth_field') === 'email') |
191
|
|
|
{ |
192
|
|
|
$this->email = $digest['username']; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
// Grab the user that corresponds to that "username" |
196
|
|
|
// exact field determined in the api config file - api.auth_field setting. |
197
|
|
|
$user = $this->user_model->as_array()->find_by( config_item('api.auth_field'), $digest['username'] ); |
198
|
|
View Code Duplication |
if (! $user) |
|
|
|
|
199
|
|
|
{ |
200
|
|
|
$this->ci->output->set_header( sprintf('WWW-Authenticate: Digest realm="%s", nonce="%s", opaque="%s"', config_item('api.realm'), $nonce, $opaque) ); |
201
|
|
|
// If an email is used, log the attempt |
202
|
|
|
if (config_item('api.auth_field') === 'email') |
203
|
|
|
{ |
204
|
|
|
$this->ci->login_model->recordLoginAttempt($digest['username']); |
205
|
|
|
} |
206
|
|
|
return false; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
// Calc the correct response |
210
|
|
|
$A1 = $user['digest_key']; |
211
|
|
|
|
212
|
|
|
if ($digest['qop'] == 'auth') |
213
|
|
|
{ |
214
|
|
|
$A2 = md5( strtoupper( $_SERVER['REQUEST_METHOD'] ) .':'. $digest['uri'] ); |
215
|
|
|
} else { |
216
|
|
|
$body = file_get_contents('php://input'); |
217
|
|
|
$A2 = md5( strtoupper( $_SERVER['REQUEST_METHOD'] ) .':'. $digest['uri'] .':'. md5($body) ); |
218
|
|
|
} |
219
|
|
|
$valid_response = md5($A1 .':'. $digest['nonce'].':'. $digest['nc'] .':'. $digest['cnonce'] .':'. $digest['qop'] .':'. $A2); |
220
|
|
|
|
221
|
|
View Code Duplication |
if ($digest['response'] != $valid_response) |
222
|
|
|
{ |
223
|
|
|
$this->ci->output->set_header( sprintf('WWW-Authenticate: Digest realm="%s", nonce="%s", opaque="%s"', config_item('api.realm'), $nonce, $opaque) ); |
224
|
|
|
// If an email is used, log the attempt |
225
|
|
|
if (config_item('api.auth_field') === 'email') |
226
|
|
|
{ |
227
|
|
|
$this->ci->login_model->recordLoginAttempt($digest['username']); |
228
|
|
|
} |
229
|
|
|
return false; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
$this->user = $user; |
233
|
|
|
|
234
|
|
|
return $user; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
//-------------------------------------------------------------------- |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Attempts to log a user into the API via the configured 'api.auth_type' |
241
|
|
|
* config variable in config/api.php. |
242
|
|
|
* |
243
|
|
|
* NOTE: Since this is intended for API use, it is a STATELESS implementation |
244
|
|
|
* and does not support remember me functionality. |
245
|
|
|
* |
246
|
|
|
* This basically replaces the login() method due to the way the AuthTrait |
247
|
|
|
* works. |
248
|
|
|
* |
249
|
|
|
* @return bool |
250
|
|
|
*/ |
251
|
|
|
public function viaRemember() |
252
|
|
|
{ |
253
|
|
|
$user = false; |
254
|
|
|
|
255
|
|
|
switch (config_item('api.auth_type')) |
256
|
|
|
{ |
257
|
|
|
case 'basic': |
258
|
|
|
$user = $this->tryBasicAuthentication(); |
259
|
|
|
break; |
260
|
|
|
case 'digest': |
261
|
|
|
$user = $this->tryDigestAuthentication(); |
262
|
|
|
break; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
// If the user is throttled due to too many invalid logins |
266
|
|
|
// or the system is under attack, kick them back. |
267
|
|
|
// We need to test for this after validation because we |
268
|
|
|
// don't want it to affect a valid login. |
269
|
|
|
|
270
|
|
|
if ($this->email) |
271
|
|
|
{ |
272
|
|
|
// If throttling time is above zero, we can't allow |
273
|
|
|
// logins now. |
274
|
|
|
if ($time = (int)$this->isThrottled($this->email) > 0) |
275
|
|
|
{ |
276
|
|
|
$this->error = sprintf(lang('api.throttled'), $time); |
277
|
|
|
return false; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
$this->email = null; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
if (! $user) |
284
|
|
|
{ |
285
|
|
|
$this->user = null; |
286
|
|
|
return $user; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
$this->loginUser($user); |
290
|
|
|
|
291
|
|
|
Events::trigger('didLogin', [$user]); |
292
|
|
|
|
293
|
|
|
return true; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
//-------------------------------------------------------------------- |
297
|
|
|
|
298
|
|
|
//-------------------------------------------------------------------- |
299
|
|
|
// Protected Methods |
300
|
|
|
//-------------------------------------------------------------------- |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* Checks the client's IP address against any IP addresses specified |
304
|
|
|
* in the api config file. If any are found, the client is refused |
305
|
|
|
* access immediately. |
306
|
|
|
*/ |
307
|
|
View Code Duplication |
public function checkIPBlacklist() |
308
|
|
|
{ |
309
|
|
|
$blacklist = explode(',', config_item('api.ip_blacklist')); |
310
|
|
|
|
311
|
|
|
array_walk($blacklist, function (&$item, $key) { |
312
|
|
|
$item = trim($item); |
313
|
|
|
}); |
314
|
|
|
|
315
|
|
|
if (in_array($this->ci->input->ip_address(), $blacklist)) |
316
|
|
|
{ |
317
|
|
|
throw new \Exception( lang('api.ip_denied'), 401); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
return true; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
//-------------------------------------------------------------------- |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* Checks the client's IP address against any IP addresses specified |
327
|
|
|
* in the api config file. If the client is not accessing the site |
328
|
|
|
* from one of those addresses then their access is denied. |
329
|
|
|
*/ |
330
|
|
View Code Duplication |
public function checkIPWhitelist() |
331
|
|
|
{ |
332
|
|
|
$whitelist = explode(',', config_item('api.ip_whitelist')); |
333
|
|
|
|
334
|
|
|
array_push($whitelist, '127.0.0.1', '0.0.0.0'); |
335
|
|
|
|
336
|
|
|
array_walk($whitelist, function (&$item, $key) { |
337
|
|
|
$item = trim($item); |
338
|
|
|
}); |
339
|
|
|
|
340
|
|
|
if (! in_array($this->ci->input->ip_address(), $whitelist)) |
341
|
|
|
{ |
342
|
|
|
throw new \Exception( lang('api.ip_denied'), 401); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
return true; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
//-------------------------------------------------------------------- |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Handles the nitty gritty of actually logging our user into the system. |
352
|
|
|
* Does NOT perform the authentication, just sets the system up so that |
353
|
|
|
* it knows we're here. |
354
|
|
|
* |
355
|
|
|
* @param $user |
356
|
|
|
*/ |
357
|
|
|
protected function loginUser($user) |
358
|
|
|
{ |
359
|
|
|
// Save the user for later access |
360
|
|
|
$this->user = $user; |
361
|
|
|
|
362
|
|
|
// Clear our login attempts |
363
|
|
|
$this->ci->login_model->purgeLoginAttempts($user['email']); |
364
|
|
|
|
365
|
|
|
// We'll give a 20% chance to need to do a purge since we |
366
|
|
|
// don't need to purge THAT often, it's just a maintenance issue. |
367
|
|
|
// to keep the table from getting out of control. |
368
|
|
|
if (mt_rand(1, 100) < 20) |
369
|
|
|
{ |
370
|
|
|
$this->ci->login_model->purgeOldRememberTokens(); |
371
|
|
|
} |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
//-------------------------------------------------------------------- |
375
|
|
|
|
376
|
|
|
//-------------------------------------------------------------------- |
377
|
|
|
// UNUSED METHOD OVERRIDES |
378
|
|
|
//-------------------------------------------------------------------- |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* Attempt to log a user into the system. |
382
|
|
|
* |
383
|
|
|
* $credentials is an array of key/value pairs needed to log the user in. |
384
|
|
|
* This is often email/password, or username/password. |
385
|
|
|
* |
386
|
|
|
* NOTE: Since this is intended for API use, it is a STATELESS implementation |
387
|
|
|
* and does not support remember me functionality. |
388
|
|
|
* |
389
|
|
|
* Valid credentials: |
390
|
|
|
* - username |
391
|
|
|
* - email |
392
|
|
|
* - realm |
393
|
|
|
* |
394
|
|
|
* @param $credentials |
395
|
|
|
* @param bool $remember |
396
|
|
|
* |
397
|
|
|
* @return bool|mixed|void |
398
|
|
|
*/ |
399
|
|
|
public function login($credentials, $remember=false) |
400
|
|
|
{ |
401
|
|
|
throw new \BadMethodCallException( lang('api.unused_method') ); |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
//-------------------------------------------------------------------- |
405
|
|
|
|
406
|
|
|
/** |
407
|
|
|
* Logs a user out and removes all session information. |
408
|
|
|
* |
409
|
|
|
* NOTE: Since this is intended for API use, it is a STATELESS implementation |
410
|
|
|
* and does not support remember me functionality. |
411
|
|
|
* |
412
|
|
|
* @return mixed |
413
|
|
|
*/ |
414
|
|
|
public function logout() |
415
|
|
|
{ |
416
|
|
|
throw new \BadMethodCallException( lang('api.unused_method') ); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
//-------------------------------------------------------------------- |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* Checks whether a user is logged in or not. |
423
|
|
|
* |
424
|
|
|
* @return bool |
425
|
|
|
*/ |
426
|
|
|
public function isLoggedIn() |
427
|
|
|
{ |
428
|
|
|
return $this->logged_in; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
//-------------------------------------------------------------------- |
432
|
|
|
|
433
|
|
|
} |
434
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.