This repository was archived by the owner on May 1, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 117
Expand file tree
/
Copy pathFileBasedAuthenticator.php
More file actions
executable file
·632 lines (562 loc) · 23.8 KB
/
FileBasedAuthenticator.php
File metadata and controls
executable file
·632 lines (562 loc) · 23.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
<?php
/**
* OWASP Enterprise Security API (ESAPI)
*
* This file is part of the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. For details, please see
* <a href="http://www.owasp.org/index.php/ESAPI">http://www.owasp.org/index.php/ESAPI</a>.
*
* Copyright (c) 2007 - 2009 The OWASP Foundation
*
* The ESAPI is published by OWASP under the BSD license. You should read and accept the
* LICENSE before you use, modify, and/or redistribute this software.
*
* @author Bipin Upadhyay <http://projectbee.org/blog/contact/>
* @created 2009
* @since 1.4
* @package ESAPI_Reference
*/
require_once dirname(__FILE__).'/../Authenticator.php';
require_once dirname(__FILE__).'/DefaultUser.php';
define('MAX_ACCOUNT_NAME_LENGTH', 250);
/**
* Reference Implementation of the FileBasedAuthenticator interface.
*
* @category OWASP
* @package ESAPI_Reference
* @copyright 2009-2010 The OWASP Foundation
* @license http://www.opensource.org/licenses/bsd-license.php New BSD license
* @version Release: @package_version@
* @link http://www.owasp.org/index.php/ESAPI
*/
class FileBasedAuthenticator implements Authenticator {
private $users;
/** The file that contains the user db */
private $userDB = null;
/** How frequently to check the user db for external modifications */
private $checkInterval = 60000;//60 * 1000;
/** The last modified time we saw on the user db. */
private $lastModified = 0;
/** The last time we checked if the user db had been modified externally */
private $lastChecked = 0;
/** Associative array of user: array(AccoundId => UserObjectReference) */
private $userMap = array();
// $passwordMap[user] = passwordHash, where the values are password hashes, with the current hash in entry 0
private $passwordMap = array();
function __construct() {
$this->users = array();
$this->logger = ESAPI::getLogger("Authenticator");
}
/**
* Clears the current User. This allows the thread to be reused safely.
*
* This clears all threadlocal variables from the thread. This should ONLY be called after
* all possible ESAPI operations have concluded. If you clear too early, many calls will
* fail, including logging, which requires the user identity.
*/
function clearCurrent() {
throw new EnterpriseSecurityException("Method Not implemented");
}
/**
* This method should be called for every HTTP request, to login the current user either from the session of HTTP
* request. This method will set the current user so that getCurrentUser() will work properly.
*
* Authenticates the user's credentials from the HttpServletRequest if
* necessary, creates a session if necessary, and sets the user as the
* current user.
*
* Specification: The implementation should do the following:
* 1) Check if the User is already stored in the session
* a. If so, check that session absolute and inactivity timeout have not expired
* b. Step 2 may not be required if 1a has been satisfied
* 2) Verify User credentials
* a. It is recommended that you use
* loginWithUsernameAndPassword(HttpServletRequest, HttpServletResponse) to verify credentials
* 3) Set the last host of the User (ex. user.setLastHostAddress(address) )
* 4) Verify that the request is secure (ex. over SSL)
* 5) Verify the User account is allowed to be logged in
* a. Verify the User is not disabled, expired or locked
* 6) Assign User to session variable
*
* @param request
* the current HTTP request
* @param response
* the HTTP response
*
* @return
* the User
*
* @throws AuthenticationException
* if the credentials are not verified, or if the account is disabled, locked, expired, or timed out
*/
function login($request, $response) {
throw new EnterpriseSecurityException("Method Not implemented");
}
/**
* Verify that the supplied password matches the password for this user. Password should
* be stored as a hash. It is recommended you use the hashPassword(password, accountName) method
* in this class.
* This method is typically used for "reauthentication" for the most sensitive functions, such
* as transactions, changing email address, and changing other account information.
*
* @param user
* the user who requires verification
* @param password
* the hashed user-supplied password
*
* @return
* true, if the password is correct for the specified user
*/
function verifyPassword($user, $password) {
throw new EnterpriseSecurityException("Method Not implemented");
}
/**
* Logs out the current user.
*
* This is usually done by calling User.logout on the current User.
*/
function logout() {
throw new EnterpriseSecurityException("Method Not implemented");
}
/**
* Creates a new User with the information provided. Implementations should check
* accountName and password for proper format and strength against brute force
* attacks ( verifyAccountNameStrength(String), verifyPasswordStrength(String, String) ).
*
* Two copies of the new password are required to encourage user interface designers to
* include a "re-type password" field in their forms. Implementations should verify that
* both are the same.
*
* @param accountName
* the account name of the new user
* @param password1
* the password of the new user
* @param password2
* the password of the new user. This field is to encourage user interface designers to include two password fields in their forms.
*
* @return
* the User that has been created
*
* @throws AuthenticationException
* if user creation fails due to any of the qualifications listed in this method's description
*/
function createUser($accountName, $password1, $password2) {
$this->loadUsersIfNecessary();
if ( !$this->isValidString($accountName) ) {
throw new AuthenticationAccountsException("Account creation failed", "Attempt to create user with null accountName");
}
if ($this->getUserByName($accountName) != null) {
throw new AuthenticationAccountsException("Account creation failed", "Duplicate user creation denied for ".$accountName);
}
$this->verifyAccountNameStrength($accountName);
if ( $password1 == null ) {
throw new AuthenticationCredentialsException( "Invalid account name", "Attempt to create account ".$accountName." with a null password" );
}
$this->verifyPasswordStrength(null, $password1);
if ($password1 != $password2) {
throw new AuthenticationCredentialsException("Passwords do not match", "Passwords for ".$accountName." do not match");
}
$user = new DefaultUser($accountName);
try {
$this->setHashedPassword( $user, $this->hashPassword($password1, $accountName) );
} catch (EncryptionException $ee) {
throw new AuthenticationException("Internal error", "Error hashing password for ".$accountName);
}
$this->userMap[$user->getAccountId()] = $user;
$this->logger->info( ESAPILogger::SECURITY, TRUE, "New user created: ".$accountName);
$this->saveUsers();
return $user;
}
/**
* Load users if they haven't been loaded in a while.
*/
protected function loadUsersIfNecessary() {
// throw new EnterpriseSecurityException("Method Not Implemented");
if (!$this->isValidString( $this->userDB )) {
$fileHandle = ESAPI::getSecurityConfiguration()->getResourceDirectory()."users.txt";
$this->userDB = fopen($fileHandle, 'a');
}
// We only check at most every checkInterval milliseconds
$now = time();
if ($now - $this->lastChecked < $this->checkInterval) {
return;
}
$this->lastChecked = $now;
$fileData = fstat($this->userDB);
if ($this->lastModified == $fileData['mtime']) {
return;
}
//Note: Removing call for now to avoid red exception and spread greenery in tests :)
// $this->loadUsersImmediately();
}
protected function loadUsersImmediately() {
throw new EnterpriseSecurityException("Method Not Implemented");
}
/**
* Saves the user database to the file system. In this implementation you must call save to commit any changes to
* the user file. Otherwise changes will be lost when the program ends.
*
* @throws AuthenticationException
* if the user file could not be written
*/
public function saveUsers() {
throw new EnterpriseSecurityException("Method Not Implemented");
}
/**
* Generate strong password that takes into account the user's information and old password. Implementations
* should verify that the new password does not include information such as the username, fragments of the
* old password, and other information that could be used to weaken the strength of the password.
*
* @param user
* the user whose information to use when generating password
* @param oldPassword
* the old password to use when verifying strength of new password. The new password may be checked for fragments of oldPassword.
*
* @return
* a password with strong password strength
*/
function generateStrongPassword($user = null, $oldPassword = null) {
$randomizer = ESAPI::getRandomizer();
$letters = $randomizer->getRandomInteger(4, 6);
$digits = 7 - $letters;
$passLetters = $randomizer->getRandomString($letters, DefaultEncoder::CHAR_PASSWORD_LETTERS );
$passDigits = $randomizer->getRandomString( $digits, DefaultEncoder::CHAR_PASSWORD_DIGITS );
$passSpecial = $randomizer->getRandomString( 1, DefaultEncoder::CHAR_PASSWORD_SPECIALS );
$newPassword = $passLetters.$passSpecial.$passDigits;
if ($this->isValidString($newPassword) && $this->isValidString($user) ) {
$this->logger->info( ESAPILogger::SECURITY, TRUE, "Generated strong password for ".$user->getAccountName());
}
return $newPassword;
}
/**
* Changes the password for the specified user. This requires the current password, as well as
* the password to replace it with. The new password should be checked against old hashes to be sure the new password does not closely resemble or equal any recent passwords for that User.
* Password strength should also be verified. This new password must be repeated to ensure that the user has typed it in correctly.
*
* @param user
* the user to change the password for
* @param currentPassword
* the current password for the specified user
* @param newPassword
* the new password to use
* @param newPassword2
* a verification copy of the new password
*
* @throws AuthenticationException
* if any errors occur
*/
function changePassword($user, $currentPassword, $newPassword, $newPassword2) {
$accountName = $user->getAccountName();
try {
$currentHash = $this->getHashedPassword($user);
$verifyHash = $this->hashPassword($currentPassword, $accountName);
if($currentHash != $verifyHash) {
throw new AuthenticationCredentialsException("Password change failed", "Authentication failed for password change on user: ".$accountName);
}
if(!$this->isValidString( $newPassword ) || !$this->isValidString($newPassword2) || $newPassword != $newPassword2) {
throw new AuthenticationCredentialsException("Password change failed", "Passwords do not match for password change on user: ".$accountName );
}
$this->verifyPasswordStrength($currentPassword, $newPassword);
//TODO: Is this actually the expected value?
$user->setLastPasswordChangeTime(time());
$newHash = $this->hashPassword($newPassword, $accountName);
if( in_array($newHash, $this->getOldPasswordHashes($user)) ) {
throw new AuthenticationCredentialsException( "Password change failed", "Password change matches a recent password for user: ".$accountName );
}
$this->setHashedPassword($user, $newHash);
$this->logger->info(ESAPILogger::SECURITY, TRUE, "Password changed for user: ".$accountName);
} catch (EncryptionException $e ) {
throw new AuthenticationException("Password change failed", "Encryption exception changing password for ".$accountName);
}
}
/**
* Returns all of the specified User's hashed passwords. If the User's list of passwords is null,
* and create is set to true, an empty password list will be associated with the specified User
* and then returned. If the User's password map is null and create is set to false, an exception
* will be thrown.
*
* @param user
* the User whose old hashes should be returned
* @param create
* true - if no password list is associated with this user, create one
* false - if no password list is associated with this user, do not create one
* @return
* a List containing all of the specified User's password hashes
*/
public function getAllHashedPasswords($user, $create) {
// TODO: Reverify with tests. Something doesn't seem right here
$hashes = $this->passwordMap[$user];
if ($this->isValidString($hashes)) {
return $hashes;
}
if ($create) {
$hashes = array();
$this->passwordMap[$user] = $hashes;
return hashes;
}
throw new RuntimeException("No hashes found for ".$user->getAccountName().". Is User.hashcode() and equals() implemented correctly?");
}
/**
* Return the specified User's current hashed password.
*
* @param user
* this User's current hashed password will be returned
* @return
* the specified User's current hashed password
*/
public function getHashedPassword($user) {
$hashes = $this->getAllHashedPasswords($user, false);
return $hashes[0];
}
/**
* Get a List of the specified User's old password hashes. This will not return the User's current
* password hash.
*
* @param user
* he user whose old password hashes should be returned
* @return
* the specified User's old password hashes
*/
public function getOldPasswordHashes($user) {
$hashes = $this->getAllHashedPasswords($user, false);
if (count($hashes) > 1) {
return array_slice($hashes, 1, (count($hashes) - 1), TRUE );
}
return array();
}
/**
* Returns the User matching the provided accountId. If the accoundId is not found, an Anonymous
* User or null may be returned.
*
* @param accountId
* the account id
*
* @return
* the matching User object, or the Anonymous User if no match exists
*/
function getUserById($accountId) {
if($accountId == 0) {
//FIXME: ANONYMOUS User to be returned
return null;
}
$this->loadUsersIfNecessary();
if( in_array($accountId, $this->userMap) ) {
return $this->userMap[$accountId];
}else {
return null;
}
}
/**
* Returns the User matching the provided accountName. If the accoundId is not found, an Anonymous
* User or null may be returned.
*
* @param accountName
* the account name
*
* @return
* the matching User object, or the Anonymous User if no match exists
*/
function getUserByName($accountName) {
if ( empty($this->users) ) {
return null;
}
if ( in_array($accountName, $this->users) ) {
return new DefaultUser($accountName, '123', '123'); // TODO: Milestone 3 - fix with real code
}
return null;
}
/**
* Gets a collection containing all the existing user names.
*
* @return
* a set of all user names
*/
function getUserNames() {
// TODO: Re-work in Milestone 3
if ( !empty($this->users) ) {
return $this->users;
}
$usersFile = dirname(__FILE__) . '/../../test/testresources/users.txt';
$rawusers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$users = array();
foreach ($rawusers as $dummy => $row) {
$row = trim($row);
if ( strlen($row) > 0 && $row[0] != '#' ) {
$user = explode('|', $row);
$users[] = $user[0];
}
}
$this->users = $users;
return $users;
}
/**
* Returns the currently logged in User.
*
* @return
* the matching User object, or the Anonymous User if no match
* exists
*/
function getCurrentUser() {
throw new EnterpriseSecurityException("Method Not implemented");
}
/**
* Sets the currently logged in User.
*
* @param user
* the user to set as the current user
*/
function setCurrentUser($user) {
throw new EnterpriseSecurityException("Method Not implemented");
}
/**
* Add a hash to a User's hashed password list. This method is used to store a user's old password hashes
* to be sure that any new passwords are not too similar to old passwords.
*
* @param user
* the user to associate with the new hash
* @param hash
* the hash to store in the user's password hash list
*/
private function setHashedPassword($user, $hash) {
$hashes = $this->getAllHashedPasswords($user, true);
$hashes[0] = $hash;
if (count($hashes) > ESAPI::getSecurityConfiguration()->getMaxOldPasswordHashes() ) {
//TODO: Verify
array_pop($hashes);
}
$this->logger->info(ESAPILogger::SECURITY, TRUE, "New hashed password stored for ".$user->getAccountName() );
}
/**
* Returns a $representation of the hashed password, using the
* accountName as the salt. The salt helps to prevent against "rainbow"
* table attacks where the attacker pre-calculates hashes for known strings.
* This method specifies the use of the user's account name as the "salt"
* value. The Encryptor.hash method can be used if a different salt is
* required.
*
* @param password
* the password to hash
* @param accountName
* the account name to use as the salt
*
* @return
* the hashed password
*/
function hashPassword($password, $accountName) {
$salt = strtolower( $accountName );
return ESAPI::getEncryptor()->hash($password, $salt);
}
/**
* Removes the account of the specified accountName.
*
* @param accountName
* the account name to remove
*
* @throws AuthenticationException
* the authentication exception if user does not exist
*/
function removeUser($accountName) {
// TODO: Change in Milestone 3. In milestone 1, this is used to clean up a test
$idx = array_search($accountName, $this->users);
if ( !empty($this->users) && $idx !== false ) {
unset($this->users[$idx]);
return true;
}
return false;
}
/**
* Ensures that the account name passes site-specific complexity requirements, like minimum length.
*
* @param accountName
* the account name
*
* @throws AuthenticationException
* if account name does not meet complexity requirements
*/
function verifyAccountNameStrength($accountName) {
if (!$this->isValidString( $accountName ) ) {
throw new AuthenticationCredentialsException("Invalid account name", "Attempt to create account with a null/empty account name");
}
if (true/*!ESAPI::getValidator()->isValidInput("verifyAccountNameStrength", $accountName, "AccountName", MAX_ACCOUNT_NAME_LENGTH, false )*/) {
throw new AuthenticationCredentialsException("Invalid account name", "New account name is not valid: ".$accountName);
}
}
/**
* Ensures that the password meets site-specific complexity requirements, like length or number
* of character sets. This method takes the old password so that the algorithm can analyze the
* new password to see if it is too similar to the old password. Note that this has to be
* invoked when the user has entered the old password, as the list of old
* credentials stored by ESAPI is all hashed.
*
* @param oldPassword
* the old password
* @param newPassword
* the new password
*
* @throws AuthenticationException
* if newPassword is too similar to oldPassword or if newPassword does not meet complexity requirements
*/
function verifyPasswordStrength($oldPassword, $newPassword) {
if(!$this->isValidString($newPassword)) {
throw new AuthenticationCredentialsException("Invalid password", "New password cannot be null" );
}
// can't change to a password that contains any 3 character substring of old password
if( $this->isValidString($oldPassword)) {
$passwordLength = strlen($oldPassword);
for($counter = 0; $counter < $passwordLength-2; $counter++) {
$sub = substr($oldPassword, $counter, 3);
if( strlen(strstr($newPassword, $sub)) > 0) {
// if( strlen(strstr($newPassword, $sub)) > -1) { //TODO: Even this works. Revisit for a more elegant solution
throw new AuthenticationCredentialsException("Invalid password", "New password cannot contain pieces of old password" );
}
}
}
// new password must have enough character sets and length
$charsets = 0;
$passwordLength = strlen($newPassword);
for($counter = 0; $counter < $passwordLength; $counter++) {
if(in_array(substr($newPassword, $counter, 1), str_split(DefaultEncoder::CHAR_LOWERS))) {
$charsets++;
break;
}
}
for($counter = 0; $counter < $passwordLength; $counter++) {
if(in_array(substr($newPassword, $counter, 1), str_split(DefaultEncoder::CHAR_UPPERS))) {
$charsets++;
break;
}
}
for($counter = 0; $counter < $passwordLength; $counter++) {
if(in_array(substr($newPassword, $counter, 1), str_split(DefaultEncoder::CHAR_DIGITS))) {
$charsets++;
break;
}
}
for($counter = 0; $counter < $passwordLength; $counter++) {
if(in_array(substr($newPassword, $counter, 1), str_split(DefaultEncoder::CHAR_SPECIALS))) {
$charsets++;
break;
}
}
// calculate and verify password strength
$passwordStrength = $passwordLength * $charsets;
if($passwordStrength < 16) {
throw new AuthenticationCredentialsException("Invalid password", "New password is not long and complex enough");
}
}
/**
* Determine if the account exists.
*
* @param accountName
* the account name
*
* @return true, if the account exists
*/
function exists($accountName) {
throw new EnterpriseSecurityException("Method Not implemented");
}
private function isValidString($param) {
return (isset($param) && $param != '');
}
}
?>