7.4. Persistent LoginsA persistent login is a mechanism that persists authentication between browser sessions. In other words, a user who logs in today is still logged in tomorrow, even if the user's session expires between visits. A persistent login diminishes the security of your authentication mechanism, but it increases usability. Instead of troubling the user to provide authentication credentials upon each visit, you can provide the user with the option of being remembered. Figure 7-2. An attacker can replay a user's cookie to gain unauthorized access
A persistent login requires a persistent login cookie, often called an authentication cookie , because a cookie is the only standard mechanism that can be used to persist data across multiple sessions. If this cookie provides permanent access, it poses a serious risk to the security of your application, so you want to be sure that the information you store in the cookie has a restricted window of time for which it can be used to authenticate. The first step is to devise a method that mitigates the risk posed by a captured persistent login cookie. Although capture is clearly something that you want to avoid, a Defense in Depth approach is best, particularly because this mechanism diminishes the security of an authentication form even when implemented correctly. Thus, the cookie should not be based upon any information that provides permanent access, such as the user's password. To avoid the use of the user's password, create a token that is valid for a single authentication: <?php $token = md5(uniqid(rand(), TRUE)); ?> You can store this token in a user's session in order to associate it with that particular user, but this doesn't help you persist logins across sessions, which is the whole point. Therefore, you must use a different method to associate a token with a particular user. Because a username is less sensitive than a password, you can store it in the cookie, and this can be used during authentication to determine which user's token is being presented. However, a slightly better approach is to use a secondary identifier that is less likely to be predicted or discovered. Consider a table for storing usernames and passwords that has three additional columns for a secondary identifier (identifier), a persistent login token (token), and a persistent login timeout (timeout): mysql> DESCRIBE users; +------------+------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +------------+------------------+------+-----+---------+-------+ | username | varchar(25) | | PRI | | | | password | varchar(32) | YES | | NULL | | | identifier | varchar(32) | YES | MUL | NULL | | | token | varchar(32) | YES | | NULL | | | timeout | int(10) unsigned | YES | | NULL | | +------------+------------------+------+-----+---------+-------+ By generating and storing a secondary identifier along with the token, you can create a cookie that does not disclose any of the user's authentication credentials: <?php $salt = 'SHIFLETT'; $identifier = md5($salt . md5($username . $salt)); $token = md5(uniqid(rand(), TRUE)); $timeout = time() + 60 * 60 * 24 * 7; setcookie('auth', "$identifier:$token", $timeout); ?> When a user presents a persistent login cookie, you can check to see that several criteria are met: <?php /* mysql_connect() */ /* mysql_select_db() */ $clean = array(); $mysql = array(); $now = time(); $salt = 'SHIFLETT'; list($identifier, $token) = explode(':', $_COOKIE['auth']); if (ctype_alnum($identifier) && ctype_alnum($token)) { $clean['identifier'] = $identifier; $clean['token'] = $token; } else { /* ... */ } $mysql['identifier'] = mysql_real_escape_string($clean['identifier']); $sql = "SELECT username, token, timeout FROM users WHERE identifier = '{$mysql['identifier']}'"; if ($result = mysql_query($sql)) { if (mysql_num_rows($result)) { $record = mysql_fetch_assoc($result); if ($clean['token'] != $record['token']) { /* Failed Login (wrong token) */ } elseif ($now > $record['timeout']) { /* Failed Login (timeout) */ } elseif ($clean['identifier'] != md5($salt . md5($record['username'] . $salt))) { /* Failed Login (invalid identifier) */ } else { /* Successful Login */ } } else { /* Failed Login (invalid identifier) */ } } else { /* Error */ } ?> You should adhere to three important implementation details to restrict the use of a persistent login cookie:
Another useful guideline is to require that the user provide a password prior to performing a sensitive transaction. The persistent login should grant access to only the features of your application that are not considered to be extremely sensitive. There is simply no substitute for requiring a user to manually authenticate prior to performing some sensitive transaction. Lastly, you want to make sure that a user who logs out is really logged out, and this includes deleting the persistent login cookie: <?php setcookie('auth', 'DELETED!', time()); ?> This overwrites the cookie with a useless value and also sets it to expire immediately. Thus, a user whose clock somehow causes this cookie to persist should still be effectively logged out. |