View login запомнить меня работы.

Раз уж подняли эту тему, хочется напомнить о том, как улучшить реализацию стандартной функции 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: это не готовое решение, а пример.