yttonk yttonk - 7 months ago 24
PHP Question

PHP Persistent Login - Regenerate Login Token

I am trying to implement a PHP persistent login solution to protect some admin pages on a website I'm working on, using this SO answer as the basis:

PHP login system: Remember Me (persistent cookie)

After Logging In

if ($login->success && $login->rememberMe) { // However you implement it
$selector = base64_encode(openssl_random_pseudo_bytes(9));
$authenticator = openssl_random_pseudo_bytes(33);

setcookie(
'remember',
$selector.':'.base64_encode($authenticator),
time() + 864000,
'/',
'yourdomain.com',
true, // TLS-only
true // http-only
);

$database->exec(
"INSERT INTO auth_tokens (selector, token, userid, expires) VALUES (?, ?, ?, ?)",
[
$selector,
hash('sha256', $authenticator),
$login->userId,
date('Y-m-d\TH:i:s', time() + 864000)
]
);
}


Re-Authenticating On Page Load

if (empty($_SESSION['userid']) && !empty($_COOKIE['remember'])) {
list($selector, $authenticator) = explode(':', $_COOKIE['remember']);

$row = $database->selectRow(
"SELECT * FROM auth_tokens WHERE selector = ?",
[
$selector
]
);

if (hash_equals($row['token'], hash('sha256', base64_decode($authenticator)))) {
$_SESSION['userid'] = $row['userid'];
// Then regenerate login token as above
}
}


My question is that I don't understand what is meant by this section in the "Re-Authenticating On Page Load" section:

// Then regenerate login token as above


Which is the login token it's referring to - does that mean this bit:

$selector = base64_encode(openssl_random_pseudo_bytes(9));


Or this bit:

$authenticator = openssl_random_pseudo_bytes(33);


And once I have done that, do I have to:


  1. Add another row to the "auth_tokens" table

  2. Re-generate the cookie to include the new token value?



I've been trying various options for persistent logins all week, and this has almost got me there, but I'm stumbling at this last block.




Update to include answer

In the end I got this to work, following advice from Scott Arciszewski (http://stackoverflow.com/users/2224584/scott-arciszewski).

I wanted to update this question to include my answer in case it helps anyone else out.

Process Login

if ($check_password == true) { // password is valid

$user_id = $row['fld_id']; // $row is from SELECT not included in this extract, checking user details from their username and password

if (isset($fld_rem)) { // remember me ticked

$selector = base64_encode(openssl_random_pseudo_bytes(9));
$authenticator = openssl_random_pseudo_bytes(33);
$token_auth = hash('sha256', $authenticator);
$exp = date('Y-m-d\TH:i:s', time() + 864000);

setcookie(
'remember',
$selector.':'.base64_encode($authenticator),
time() + 864000,
'/',
'localhost',
false, // TLS-only
true // http-only
);

// clear out old session data for this user
$sql = "DELETE FROM auth_tokens WHERE userid = :userid";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':userid', $user_id);
$stmt->execute();

// create a new session row for this user
$sql = "INSERT INTO auth_tokens (selector, token, userid, expires) VALUES (:selector, :token_auth, :userid, :exp)";

$stmt = $pdo->prepare($sql);

$stmt->bindParam(':selector', $selector);
$stmt->bindParam(':token_auth', $token_auth);
$stmt->bindParam(':userid', $user_id);
$stmt->bindParam(':exp', $exp);

$stmt->execute();

session_start();
$_SESSION['userid'] = $user_id;

header('Location:admin-home.php#logged-in-remember-me-ticked');
exit;

} else { // remember me NOT ticked

session_start();
$_SESSION['userid'] = $user_id;

header('Location:admin-home.php#logged-in-remember-me-not-ticked');
exit;

}

} else { // password is not valid

$html = "<p>Login failed</p>";

}


Validation

The validation code is included at the top of every page I want to secure. If there is no "userid" session then the user is redirected to the login page.

Once the user has logged in, the "userid" session is created.

If the user clicked "remember me" then the "remember" cookie is created.

Then, when the user closes the browser, the "userid" session is ended, but the "remember" cookie is used to recreate a new "userid" session, as well as re-loading the current session details in the "auth_tokens" table.

<?php
// http://stackoverflow.com/questions/3128985/php-login-system-remember-me-persistent-cookie
// http://stackoverflow.com/questions/12091951/php-sessions-login-with-remember-me

session_start();

if (empty($_SESSION['userid']) && empty($_COOKIE['remember'])) { // No Userid Session and no Cookie, go back to homepage

header('Location:login.php?issue=no_cookie_no_session');
exit;

} elseif (empty($_SESSION['userid']) && !empty($_COOKIE['remember'])) { // No Userid Session, but Remember Cookie exists - regenerate everything and let user in

list($selector, $authenticator) = explode(':', $_COOKIE['remember']);

$sql = "SELECT userid, token FROM auth_tokens WHERE selector = :sel";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':sel', $selector);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);

if (hash_equals($row['token'], hash('sha256', base64_decode($authenticator)))) { // token in cookie = token in database

// Set the session id since this has been successful
$_SESSION['userid'] = $row['userid'];

// regenerate login token
$user_id = $row['userid'];
$selector = base64_encode(openssl_random_pseudo_bytes(9));
$authenticator = openssl_random_pseudo_bytes(33);
$token_auth = hash('sha256', $authenticator);
$exp = date('Y-m-d\TH:i:s', time() + 864000);

setcookie(
'remember',
$selector.':'.base64_encode($authenticator),
time() + 864000,
'/',
'localhost',
false, // TLS-only
true // http-only
);

// clear out old session data for this user
$sql = "DELETE FROM auth_tokens WHERE userid = :userid";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':userid', $row['userid']);
$stmt->execute();

// create a new session row for this user
$sql = "INSERT INTO auth_tokens (selector, token, userid, expires, added) VALUES (:selector, :token_auth, :userid, :exp, now())";

$stmt = $pdo->prepare($sql);

$stmt->bindParam(':selector', $selector);
$stmt->bindParam(':token_auth', $token_auth);
$stmt->bindParam(':userid', $user_id);
$stmt->bindParam(':exp', $exp);

$stmt->execute();

} else { // token in cookie != token in database

header('Location:login.php?ref=1');
exit;

}

}
?>

Answer

When you login the first time, if you check the "remember me" box, two things will happen:

  1. Their current HTTP session will be updated with whatever information is needed to tie the session to an active user account.
  2. A cookie will be placed on the user's computer containing the long-term authentication token.

The token consists of selector:verifier as a concatenated string.

The next time your user comes to your website, if they don't have an active session, the cookie will be used to automatically log them in as the associated user. This token should then be discarded on both sides (browser and database) and a new one should be generated to take its place.

The end result is: To end users, it's like they're logged in forever (until they explicitly log out, which should invalidate the tokens).

Comments