/program/lib/loginlib.php -- functions to handle user login/logout
Visitors need to authenticate when they want to see a 'protected' area or when they want to modify the website content. This requires a user account and the visitor presenting valid credentials (username + password).
We don't want malicious scripts trying to get in with brute force. However, we need to accomodate users that make typo's while entering credentials. Also we want to allow for sending password reminders, in a safe way.
Features:
T1 = $CFG->login_failures_interval, default 12 minutes
T2 = $CFG->login_bypass_interval, default 30 minutes
T3 = $CFG->login_blacklist_interval, default 8 minutes
Once a user is authenticated, a PHP-session is established, using our own database based session handler. The session key is stored in a cookie in the user's browser. Presenting this cookie on subsequent calls is enough to gain access. The logout routine takes care of killing both the user's cookie and the session in the database.
There are several different login procedures.
3. Forgotten password, phase 1: sending a laissez-passer The user presents a valid combination of username and email address. Subsequently a one-time logon-code (dubbed 'laissez-passer') is sent to the user's email address. This code is valid for at most T2 minutes. This code can be used, exactly once, to send a temporary password via email.
4. Forgotten password, phase 2: sending a temporary password The user clicks the link received in phase 1 and a temporary password (dubbed 'bypass') is sent to the user. This temporary password is valid for another T2 minutes.
5. Message box This is a pseudo-procedure. A simple 'message box' type of screen is displayed but no real interaction is anticipated via this screen. This is used to tell the user that things didnt work out (too many failures) or to check their mail for further instructions (e.g. when a laissez passer was sent). Whenever the user acknowledges this screen by clicking the button, she usually is directed to $WAS_SCRIPT_NAME.
6. Blacklist This is also a pseudo-procedure. The corresponding number is used to identify blacklisted IP-addresses in the database.
Note that when the user logs in after a temporary password has been sent, the normal login procedure is immediately followed by a (forced) 'change password' procedure. This makes sure that a temporary password will be changed immediately after the user logs in.
Note that each of the procedures can be entered 'manually', i.e. by opening index.php?login=X the user starts procedure X. This allows for the user to change her password whenever she feels this is necessary, without going through the trouble of the 'forgotten password'-procedure which eventually ends with the user changing her password too.
this selects authentication via username+email in authenticate_user()
this selects authentication via username+laissez_passer in authenticate_user()
this selects authentication via username+password in authenticate_user()
this is the number of seconds to delay responding after a login action fails (slow 'm down..)
this is a pseudo procedure, used to record blacklisted IP-addresses
this is the procedure to change the user's password
this is a pseudo procedure, used to deliver some message to the user
this is the usual procedure for logging in
this is phase 2 of the 'forgot password' procedure
this is phase 1 of the 'forgot password' procedure
this only shows the login dialog
this is the hardcoded minimal number of digits in a new password
this hardcoded minimal length is enforced whenever a user wants to change her password
this is the hardcoded minimal number of lower case characters in a new password
this is the hardcoded minimal number of upper case characters in a new password
check the new passwords satisfy password requirements
Users should provide the same password twice, to prevent typo's, so both passwords should be equal. Also, the following requirements should be satisfied:
The minimum password length and other minimum values are not configurable (via $CFG) because that would make it too easy (too tempting) to give in and use weak passwords (too short, only lowercase, etc.) However, if your really MUST, you could change the MINIMUM_PASSWORD_* constants defined above.
Note that the check agains existing (temporary and regular) passwords is not performed if the corresponding parameters are empty. If they are empty, this routine only performs the first 4 checks in the list above.
check the user's credentials in one of three ways
This authenticates the user's credentials. There are some variants:
After that we check the validity of the token:
Because there are actually several checks to be done, we decided not to use SQL like: SELECT * FROM users WHERE username=$username AND password=$password, not the least because we need to have the salt in our hands before we can successfully compare password hashes.
Note: The 'special cases' (checking email, checking laissez_passer, checking bypass) all have their token stripped from leading and trailing spaces. We don't want to further confuse the user by not accepting a spurious space that was entered in the heat of the moment when the user has 'lost' her password. Therefore we also always trim the username. Rationale: usernames and also the generated passwords etc. never have leading/trailing spaces. However, one cannot be sure that a user has not entered a real password with leading/trailing space, so we do NOT trim the $token in the first attempt in the case 'BY_PASSWORD' below.
update the users database with a new (randomly salted) password and reset bypass mode to normal
This updates the user record for user with user_id and stores the new password. The new password and a new random salt are hashed together and the result is stored, together with the new salt, overwriting the old salt and the old password hash. The bypass mode is reset to normal and the bypass hash is reset. Return TRUE on success.
construct a standard dialog definition for a specific login procedure
There are 5 different dialogs, each with 1, 3 or 5 inputs, depending on $dialog
Field Used in dialogs: login_username 1 2 3 4 login_password 1 2 login_new_password1 2 login_new_password2 2 login_email 3 login_laissez_passer 4 button 1 2 3 4 5
Note: There is some discussion about the autocomplete flag. Before HTML5 it was a browser-specific feature that sometimes didn't work. However, we can at least try to prevent it by adding autocomplete="off" to password fields.
add remote_addr to the blacklist for specified interval (in seconds)
delay execution of this script for a few seconds and blacklist the remote_addr during the delay
This immediately blacklists the remote address for LOGIN_FAILURE_DELAY_SECONDS seconds. Once that is done, the execution is delayed for that same period of time. After the delay, the temporary blacklisting is removed from the table. The whole purpose of this rapid succession of an INSERT and a DELETE is to prevent brute force attack scripts that do not wait for an answer and/or use multiple connections. This routine defeats that trick, because nothing can be done when an IP-address is blacklisted.
add 1 point to score for a particular IP-address and failed procedure, return the new score
This records a login failure in a table and returns the the number of failures for the specified procedure in the past T1 minutes.
deactivate all login failures/blacklisting scores for remote_addr
This resets all the scores for all failed login attempts and blacklistings for the specified IP-addres. The records in the login_failures table are deactivated by deleting the records for this remote_addr.
Note that the failed logins and the blacklistings are recorded in the log_messages table via logger(). Therefore we can automatically keep this table 'login_failures' clean without cron jobs.
This routine resets _all_ scores, including any blacklisting that might still be active, i.e. which has a datim in the future.
find out if a remote address is blacklisted at this time
This routine checks if this remote address is blacklisted in the login_failures table with a datim that lies in the future. If this is the case, the address is indeed blacklisted and TRUE is returned. Note that we sum the points much the same way as in login_failure_increment rather than counting 'blacklist-records'.
construct an action attribute propagating existing parameters
this routine constructs an action value of a form keeping any existing information from $_SERVER['PATH_INFO'] and/or $_GET[], perhaps with additional parameters from the $add-array.
The object of this excercise is to allow the distribution of links like http://exemplum.eu/index.php/area/2/Intranet.html that actually work by first letting the user login (via show_login() etc) and subsequently falling through to the correct page in a single pass. This requires that we keep all parameters the user presented initially, including the PATH_INFO and thE $_GET-parameters. However, this routine also adds the parameters in $add to the mix, allowing the user to return to the login-routine when she is posting her credentials. In other words: we attempt to propagate the place the user wants to navigate to after logging in to the next screen.
Note that it would be weird to propagate the parameter 'logout' while attempting to login; it yields an endless loop and it becomes impossible to login from the screen that is presented. Therefore we unconditionally get rid of the 'logout' parameter while copying $_GET[]. The same logic applies to the login parameter: if we need it to return to loginlib, it should be set (again) via $add; we do not propagate that field from $_GET[]. Finally, those parameters can also occur in $path_info, so we cleanup that string too.
If $propagate is FALSE we don't propagate after all. However, in this case we do combine $path and $add to a single usable href.
send a new (temporary) password to the user via email
This generates a new temporary password for the user, stores it in the user record and sends an email message to the user with the temporary password (in plain text) and further instructions.
Note that the password is valid only for a limited time; sending a password in plain text appears to be an acceptable risk. Note that the limited time is increased with 10% in order to give the user a reasonable margin to enter the correct password.
Also note that the existing salt is used to salt the temporary password; this makes it easier to check for validity of both the regular password and the temporary password lateron.
A log message recording the event is added via logger().
send email to user confirming password change
This sends an email to the user's email addres confirming that the user's password was changed. Note that the new password is _NOT_ sent to the user.
send a special one-time login code to the user via email
This generates a temporary code with which the user can request a new temporary password. This code can be used only once. Note that this code is valid for only a limited time. This code simply overwrites the bypass password (the temporary password) in the user record. This means that if a phase 2 is pending, a new phase 1 will replace the old phase 2.
The temporary code consists of digits and uppercase characters. However, it is longer (20 characters) than the minimum password length of 6, so a brute force on such a code will likely not succeed (36^20 is much more than the usual 62^6).
This routine also brings the user's record into 'bypass mode'. This mode is reset to 'normal' after the user has successfully changed her password.
A log message recording the event is added via logger().
check equivalency of salt+password against hash
This verifies whether the hash of $salt and $password is the same as $hash. Note that the two hashes are compared in a caseINsensitive way. Usually these hashes are using lowercase hexadecimal digits but a caseINsensitive compare makes A,...,F equivalent to a,...,f.
If the length of the presented $hash is 40 characters, it is assumed that the hash algorithm to use is sha1, otherwise the default algorithm (md5) is used.
generate a quasi random string to salt the password hash
this generates a quasi-randomg string of digits and letters to be used as a salt when calculating a password hash.
show complete login dialog and exit
There are different variations of this dialog.
(message) Username: _____ Password: _____ [OK]
The link <forgotten password?> takes the user directly to screen #3 LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER.
2. LOGIN_PROCEDURE_CHANGE_PASSWORD - Login/change password
(message) Username: _____ Old password: _____ New password1: _____ New password2: _____ [OK]This screen is used to change the user's password. If both new passwords are different, the user is redirected to the same screen #3 until she gets it right. Otherwise, if the old password is either the valid original password OR the bypass password, the password is changed and the mode is reset to 'normal'. The one-time codes and the bypass password are reset. Also, as a result, the user is logged in. If the user failed to enter the proper old password more than N times, the mode is also reset to normal (invalidating the laissez-passer and the bypass password) and the user is dropped at a screen basically telling her to contact the webmaster. In this process the user is also logged out if necessary.
3. LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER Request bypass
(message) Username: _____ Email: _____ [OK]This screen is used to help the user reset her password. It is displayed automatically after N failed login attempts. This screen can also be reached via the <forgot password?> link in screen #1.
If the user presents an invalid combination of username and email address, this failure is also recorded. If the number of failures has reached the threshold N, the user is taken to a screen that basically tells the user ask the webmaster for assistance and that's that.
If the user presents a valid combination of username and email address, an email with a message like 'click the link below for a new password' is sent to the email address. After that mail is sent a screen is displayed, basically telling the user to await further instructions that were sent via mail.
Note that resetting the password is a two-step process. First the user is sent a one-time code laissez-passer embedded in a link. Clicking the link before it expires (after T2 minutes) yields a second emai message containing a bypass password that can be used to login and subsequently change the primary password. After that both the laissez-passer and the bypass password are invalidated.
4. LOGIN_PROCEDURE_SEND_BYPASS - Send a temporary password
(message) Username: _____ One-time code: _____ [OK]Phase 2 of the forgot password procedure.
5. LOGIN_PROCEDURE_MESSAGE_BOX Alert
(message) [OK]
This screen is user to communicate various messages to the user, e.g. 'check your mail for instructions', 'contact webmaster', etc.
Note: the $message receives special treatment. The actual message is prefixed with the word 'Message: ' and the combination is wrapped within a B-tag and a SPAN. By carefully changing the style of those tags, we end up either with the bare $message with a yellow background if style is ON in the user's browser, OR 'Message: $message in bold face if the style is OFF. This gives good results in a text browser, eg. lynx.
execute the selected login procedure
The login process is controlled via the parameter 'login' provided by the user via 'index.php?login=N or via the 'action' property in a HTML-form. These numbers correspond to the LOGIN_PROCEDURE_* constants defined near the top of this file. Here's a reminder:
Note that this routine only returns to the caller after either a succesful regular login (i.e. after completing LOGIN_PROCEDURE_NORMAL). All the other variants and error conditions yield another screen and an immediate exit and hence no return to caller. If this routine returns, it returns the user_id of the authenticated user (the primary key into the users table). It is up to the caller to retrieve additional information about this user; any information read from the database during login is discarded. This prevents password hashes still lying around.
Note that a successful login has the side effect of garbage collection: whenever we experience a successful login any obsolete sessions are removed. This makes sure that locked records eventually will be unlocked, once the corresponding session no longer exists. The garbage collection routine is also called from the PHP session handler every once in a while, but here we make 100% sure that garbage is collected at least at every login. (Note: obsolete sessions should not be a problem for visitors that are not logged in, because you have to be logged in to be able to lock a record.)
Note that in LOGIN_PROCEDURE_SEND_BYPASS we refrain from propagating the parameters. because the user might arrive here with GET['code'] and GET['username']. We don't want to propagate that. Side effect is that nothing is propagated, but what's the point anyway when you are busy resetting your password. Not having parameters propagate is not important at that time.
end a session (logout the user) and maybe redirect
This routine ends the current session if it exists (as indicated by the cookie presented by the user's browser). An empty value is sent to the browser (effectively deleting the cookie) and also the session is ended. The routine ends either with showing a generic login dialog OR a redirection to a user-defined page.
Note that as a rule this routine does NOT return but instead calls exit(). However, there are cases where this routine DOES return, notably when no session appears to be established (no cookie submitted by the browser. If the routine does return, the status is equivalent to a logged out user; no session exists so the user simply should not be logged in.
On successful logout, the user usually is redirected to her own redirect-page. Thos depends on the $redirect_flag: if it is FALSE we always force a normal login dialog even if $redirect is specified. This is used from was_login() to make sure that we end in a login dialog after (forcefully) logging out the user. This also gets rid of the 'logout=' parameter preventing a logout loop.
calculate a hash from a salt and a password
This routine constructs a hash of the combination of salt and password. By default the md5() function is used to calculate a 32-character long string of hexadcimal digits. If the parameter $algorithm is 1 then the sha1() function is used and a 40-character long string of hexadecimal digits is returned.
Note that we do not use the crypt() function because that could introduce a portability issue. If a website is migrated to another machine, the used crypt algoritm might no longer be available, and that would effectively lock out all users. Both md5() and sha1() are standard PHP-functions (since 4.3.x) and should be portable, which makes any installed table of users portable too.
Note that this routine used to be called 'password_hash'. PHP 5.5.0 introduced a function called 'password_hash' which clashed with our own version, hence we renamed this function to was_password_hash().
Documentation generated on Tue, 28 Jun 2016 19:10:07 +0200 by phpDocumentor 1.4.0