/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()
useful when debugging routines in this file: 0=production, 1=debugging
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 defines the maximum line length in messages and instructions
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.
close the login dialog/table and maybe an opened HTML-form
add a row with links to home page and forgot password dialog to the login dialog/table
This constructs a link to the home page in the left hand dialog/table column and optionally a link to the start of the forgot password dialog. The latter is only displayed if $forgot is not empty.
add a row to the table/dialog with wordwrap()'ed instruction for the user
construct the start of the login dialog, opening the form and the secondary table
This optionally opens an HTML-form (which is optionally closed in companion routine login_dialog_close()) and subsequently starts a table with two columns. The first row of the table shows the $title, the second row may show a wordwrap()'ed feedback message for the user in a different background colour.
add a row with a password input field to the login dialog/table
This generates HTML for another table row. Special feature: try to suppress autocomplete via an extra parameter. See
add a row with a submit button to the login dialog/table
add a row with an ordinary input field to the login dialog/table
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 recorede 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 the end of the simple HTML-page, closing the full size table
construct the start of a simple HTML-page and open a full size table
This routine starts with a plain HTML-page, with a title and possibly a single line of Javascript to place the cursor in a particular input field (defined later in the page). After that, a main table is opened and within that 1x1 table the table cell is opened. login_page_close() closes that cell.
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().
a simple in-line style sheet conveniently grouped in a single routine
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.
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 N1 times, the mode is also reset to normal (invalidating the laissez-passer and the bypass password) and the user is dropped at a screen #4 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 N1 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 N2, the user is taken to a screen #4 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 #4 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 T 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
Phase 2 of the forgot password procedure.
5. LOGIN_PROCEDURE_MESSAGE_BOX Alert
(message)
This screen is user to communicate various messages to the user, e.g. 'check your mail for instructions', 'contact webmaster', etc.
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 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.)
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 or a non-existing/expired session). 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.
Documentation generated on Wed, 11 May 2011 23:45:21 +0200 by phpDocumentor 1.4.0