Раз уж подняли эту тему, хочется напомнить о том, как улучшить реализацию стандартной функции remember-me.
Оба предложенных ранее варианта не учитывают один важный момент, что, если token будет похищен? При обычной реализации аутентификации через remember me token, атакующий, получив такой токен, получит доступ к сайту на неограниченное время, а жертва даже не узнает о факте хищения…
Что же делать?
Barry Jaspan предложил улучшенный вариант remember-me аутентификации.Вкратце, добавляется еще один тип токена - series. Генерировать его нужно случайно, можно так же, как и обычный token. Главное отличие его от токена, это то, что он не меняется после успешной аутентификации через токен.
Таблица для хранения:
CREATE TABLE test.one_time_auth(token CHAR (32),
series CHAR (32),
user_id INT (11) UNSIGNED NOT NULL,
expire DATETIME DEFAULT NULL,
PRIMARY KEY (series))
ENGINE = INNODB
И класс с примером
setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$auth = new one_time_auth($db);
list($token, $series) = $auth->remember(10, null, "2010-12-31");
$user_id = $auth->remind($token, $series);
list($token, $series) = $auth->remember(10, $series, "2010-12-31");
$user_id = $auth->remind($token, $series);
echo $user_id;
list($token, $series) = $auth->remember(10, $series, "2010-12-31");
try {
$user_id = $auth->remind("wrongone", $series);
} catch (ThiefAssumedException $e) {
echo "We think your cookie was stolen. Please, log in again.";
}
$user_id = $auth->remind("wrongone", "wrongone"); // do nothing
class ThiefAssumedException extends Exception {}
class one_time_auth
{
/**
* @var PDO
*/
private $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
public function remember($user_id, $series = null, $expire = null)
{
$sql = "INSERT INTO one_time_auth (token, series, user_id, expire) VALUES (:token, :series, :user_id, :expire)";
$stmt = $this->db->prepare($sql);
while (true) {
try {
$stmt->execute(array(":token" => $token = $this->generateToken(),
":series" => $series = $series == null ? $this->generateToken() : $series,
"user_id" => $user_id,
"expire" => $expire));
break;
} catch (PDOException $e) {}
}
return array($token, $series);
}
public function remind($token, $series)
{
$sql = "SELECT user_id, token
FROM one_time_auth
WHERE series = :series
AND (expire IS NULL OR expire >= NOW())
LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute(array("series" => $series));
if ($row = $stmt->fetch()) {
if ($row["token"] != $token) {
$stmt = $this->db->prepare("DELETE FROM one_time_auth WHERE user_id = :user_id");
$stmt->execute(array("user_id" => $row["user_id"]));
throw new ThiefAssumedException();
}
$stmt = $this->db->prepare("DELETE FROM one_time_auth WHERE series = :series");
$stmt->execute(array("series" => $series));
return $row["user_id"];
}
}
private function generateToken()
{
return md5(uniqid("", true));
}
}
Как это работает?
У пользователя в куках сохраняются token и series, скрипт сверяет их с теми, что предоставлены в базе данных. Если они совпадают, то аутентификация успешна. Пользователь получает новый token с предыдущим series. Если token разный, а series один и тот же, то удаляем все remember-me записи для этого аккаунта и сообщаем пользователю о том, что, возможно, его токен был похищен.Ps: это не готовое решение, а пример.
Раз уж подняли эту тему, хочется напомнить о том, как улучшить реализацию стандартной функции remember-me.
Оба предложенных ранее варианта не учитывают один важный момент, что, если token будет похищен? При обычной реализации аутентификации через remember me token, атакующий, получив такой токен, получит доступ к сайту на неограниченное время, а жертва даже не узнает о факте хищения…
Что же делать?
Barry Jaspan предложил улучшенный вариант remember-me аутентификации.Вкратце, добавляется еще один тип токена - series. Генерировать его нужно случайно, можно так же, как и обычный token. Главное отличие его от токена, это то, что он не меняется после успешной аутентификации через токен.
Таблица для хранения:
CREATE TABLE test.one_time_auth(token CHAR (32),
series CHAR (32),
user_id INT (11) UNSIGNED NOT NULL,
expire DATETIME DEFAULT NULL,
PRIMARY KEY (series))
ENGINE = INNODB
И класс с примером
setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$auth = new one_time_auth($db);
list($token, $series) = $auth->remember(10, null, "2010-12-31");
$user_id = $auth->remind($token, $series);
list($token, $series) = $auth->remember(10, $series, "2010-12-31");
$user_id = $auth->remind($token, $series);
echo $user_id;
list($token, $series) = $auth->remember(10, $series, "2010-12-31");
try {
$user_id = $auth->remind("wrongone", $series);
} catch (ThiefAssumedException $e) {
echo "We think your cookie was stolen. Please, log in again.";
}
$user_id = $auth->remind("wrongone", "wrongone"); // do nothing
class ThiefAssumedException extends Exception {}
class one_time_auth
{
/**
* @var PDO
*/
private $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
public function remember($user_id, $series = null, $expire = null)
{
$sql = "INSERT INTO one_time_auth (token, series, user_id, expire) VALUES (:token, :series, :user_id, :expire)";
$stmt = $this->db->prepare($sql);
while (true) {
try {
$stmt->execute(array(":token" => $token = $this->generateToken(),
":series" => $series = $series == null ? $this->generateToken() : $series,
"user_id" => $user_id,
"expire" => $expire));
break;
} catch (PDOException $e) {}
}
return array($token, $series);
}
public function remind($token, $series)
{
$sql = "SELECT user_id, token
FROM one_time_auth
WHERE series = :series
AND (expire IS NULL OR expire >= NOW())
LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute(array("series" => $series));
if ($row = $stmt->fetch()) {
if ($row["token"] != $token) {
$stmt = $this->db->prepare("DELETE FROM one_time_auth WHERE user_id = :user_id");
$stmt->execute(array("user_id" => $row["user_id"]));
throw new ThiefAssumedException();
}
$stmt = $this->db->prepare("DELETE FROM one_time_auth WHERE series = :series");
$stmt->execute(array("series" => $series));
return $row["user_id"];
}
}
private function generateToken()
{
return md5(uniqid("", true));
}
}
Как это работает?
У пользователя в куках сохраняются token и series, скрипт сверяет их с теми, что предоставлены в базе данных. Если они совпадают, то аутентификация успешна. Пользователь получает новый token с предыдущим series. Если token разный, а series один и тот же, то удаляем все remember-me записи для этого аккаунта и сообщаем пользователю о том, что, возможно, его токен был похищен.Ps: это не готовое решение, а пример.