Раз уж подняли эту тему, хочется напомнить о том, как улучшить реализацию стандартной функции remember-me.
Оба предложенных ранее варианта не учитывают один важный момент, что, если token будет похищен? При обычной реализации аутентификации через remember me token, атакующий, получив такой токен, получит доступ к сайту на неограниченное время, а жертва даже не узнает о факте хищения…
Barry Jaspan предложил улучшенный вариант remember-me аутентификации.
Вкратце, добавляется еще один тип токена — series. Генерировать его нужно случайно, можно так же, как и обычный token. Главное отличие его от токена, это то, что он не меняется после успешной аутентификации через токен.
На примере кода от zerkms:
Таблица для хранения:
И класс с примером
У пользователя в куках сохраняются token и series, скрипт сверяет их с теми, что предоставлены в базе данных. Если они совпадают, то аутентификация успешна. Пользователь получает новый token с предыдущим series. Если token разный, а series один и тот же, то удаляем все remember-me записи для этого аккаунта и сообщаем пользователю о том, что, возможно, его токен был похищен.
ps: это не готовое решение, а пример.
Оба предложенных ранее варианта не учитывают один важный момент, что, если token будет похищен? При обычной реализации аутентификации через remember me token, атакующий, получив такой токен, получит доступ к сайту на неограниченное время, а жертва даже не узнает о факте хищения…
Что же делать?
Barry Jaspan предложил улучшенный вариант remember-me аутентификации.
Вкратце, добавляется еще один тип токена — series. Генерировать его нужно случайно, можно так же, как и обычный token. Главное отличие его от токена, это то, что он не меняется после успешной аутентификации через токен.
На примере кода от zerkms:
Таблица для хранения:
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
И класс с примером
<?php
$db = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', '');
$db->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: это не готовое решение, а пример.