I think I've found a clever solution!
Advantages of this (complicated?) script:
When the user successfully logs in with Remember Me checked, a login cookie is issued in addition to the standard session management cookie.[2]
The login cookie contains the user's username, a series identifier, and a token. The series and token are unguessable random numbers from a suitably large space. All three are stored together in a database table.
When a non-logged-in user visits the site and presents a login cookie, the username, series, and token are looked up in the database.
If the triplet is present, the user is considered authenticated. The used token is removed from the database. A new token is generated, stored in database with the username and the same series identifier, and a new login cookie containing all three is issued to the user.
If the username and series are present but the token does not match, a theft is assumed. The user receives a strongly worded warning and all of the user's remembered sessions are deleted.
If the username and series are not present, the login cookie is ignored.
I've made a table in the database with the following information:
session | token | username | expire
The remember me cookie will have this setup:
$value = "$session|$token|$userhash"; //Total length = 106
Session
will be a string of 40 (sha1) characters.
Token
will be a string of 32 (md5) characters.
Userhash
in the cookie will be a string of 32 (md5 of username) characters.
Username
in the database will be the normal username.
Expire
will be now + 60 days.
The script:
ini_set('session.hash_function', 'sha1');
ini_set('session.hash_bits_per_character', '4');
session_start();
if(isset($_POST['user']) && isset($_POST['password'])) {
if(isset($_COOKIE['remember']) && strlen($_COOKIE['remember']) == 106) {
//THERE is a cookie, which is the right length 40session+32token+32user+2'|'
//Now lets go check it...
//How do I protect this script form harmful user input?
$plode = explode('|', $_COOKIE['remember']);
$session = htmlspecialchars($plode[0]);
$token = htmlspecialchars($plode[1]);
$userhash = htmlspecialchars($plode[2]);
$result_query = $auth->query("SELECT user
FROM sessions
WHERE session = '$session'
AND token = '$token'
AND user = '$userhash'");
$result_array = array();
$auth_query = $auth->query("SELECT user FROM sessions WHERE session = '$session' AND user = '$userhash'");
$auth_array = array();
while ($result_object = $result_query->fetch(PDO::FETCH_NUM)) {
$result_array[] = $result_object;
}
while ($auth_object = $auth_query->fetch(PDO::FETCH_NUM)) {
$auth_array[] = $auth_object;
}
if(count($result_array) > 0){
if(isset($_COOKIE['PHPSESSID'])) {
//COOKIE is completely valid!
//Make a new cookie with the same session and another token.
$newusername = $auth_array[0][0];
$newsession = $session;
$newtoken = md5(uniqid(rand(), true));
$newuserhash = $newusername;
$value = "$newsession|$newtoken|$newuserhash";
$expire = time() + 4184000;
setcookie('remember', $value, $expire, '/', 'spigotpool.ml', isset($_SERVER["HTTPS"]), true);
$auth_query = $auth->prepare("UPDATE sessions
SET token = :newtoken, expire=:expire
WHERE session = :session
AND token = :token
AND user = :userhash");
$auth_query->bindParam(':newtoken', $newtoken);
$auth_query->bindParam(':expire', $expire);
$auth_query->bindParam(':session', $session);
$auth_query->bindParam(':token', $token);
$auth_query->bindParam(':userhash', $userhash);
$auth_query->execute();
//Set-up the whole session (with user details from database) etc...
}
} else if(count($auth_array) == 1) {
//TOKEN is different, session is valid
//This user is probably under attack
//Put up a warning, and let the user re-validate (login)
//Remove the whole session (also the other sessions from this user?)
} else {
//Cookie expired in database? Unlikely...
//Invalid in what way?
//Make a new cookie with the same session and another token.
$newusername = $_POST['user'];
$newsession = session_id();
$newtoken = md5(uniqid(rand(), true));
$newuserhash = md5($newusername);
$value = "$newsession|$newtoken|$newuserhash";
$expire = time() + 4184000;
setcookie('remember', $value, $expire, '/', 'www.example.com', isset($_SERVER["HTTPS"]), true);
$auth->query("INSERT INTO sessions (token, expire, session, user) VALUES ('$newtoken', '$expire', '$newsession', '$newuserhash')");
header('Location: index.php?action=logged-in');
}
} else {
//No cookie, rest of the script
//Make a new cookie with the same session and another token.
$newusername = $_POST['user'];
$newsession = session_id();
$newtoken = md5(uniqid(rand(), true));
$newuserhash = md5($newusername);
$value = "$newsession|$newtoken|$newuserhash";
$expire = time() + 4184000;
setcookie('remember', $value, $expire, '/', 'www.example.com', isset($_SERVER["HTTPS"]), true);
$auth->query("INSERT INTO sessions (token, expire, session, user) VALUES ('$newtoken', '$expire', '$newsession', '$newuserhash')");
header('Location: index.php?action=logged-in');
}
}
Advantages of the script:
- Multiple login. You can create new sessions for each computer you're on.
- Cookie and database will stay clean. Active users renew there cookie every login.
- The session check at the beginning ensures that the database will not get useless requests.
- If an attacker steals a cookie, it gets a new token, but not a new session. So when the real user visits the website with the old(invalid) token but WITH a valid user-session combination the user gets a warning of the potential theft. After re-validating by logging in a new session is created and the session the attacker holds is invalid. The re-validating ensures the victim really is the victim, and not the attacker.
Reference: http://jaspan.com/improved_persistent_login_cookie_best_practice