Давид Мзареулян - Правильная постоянная авторизация
Jun. 15th, 2012
07:45 pm - Правильная постоянная авторизация
Сейчас весь интернет увлечённо обсуждает, как правильно хранить пароли. Дело хорошее:) Но я недавно заинтересовался похожей темой и обнаружил, что по ней куда меньше материалов. Тема эта — постоянная авторизация или, проще говоря, функция «Запомнить меня» при входе на сайт.
Всё нижеописанное основано на двух статьях, популярных в англонете, но почему-то в рунете никак не освещённых — это “Persistent Login Cookie Best Practice” (2004) и “Improved Persistent Login Cookie Best Practice” (2006).
Итак, к делу. Понятно, что для постоянной авторизации у пользователя на компьютере должно что-то храниться между визитами на сайт. Штатный механизм для этого — куки. Также всем, надеюсь, понятно, что куки небезопасны. Во-первых, их можно украсть — злоумышленник может подслушать трафик, может скачать файлы с куками через трояна или просто получив физический доступ к компьютеру — способов масса. Во-вторых, их можно подделать — злоумышленник может вставить скопированную куку в свой запрос или изменить её произвольным образом. Это надо учитывать при проектировании протокола.
Варианты с хранением в куке просто логина юзера или (да, так бывает!) логина и пароля, мы рассматривать не будем из уважения к читателю. Если вы увидите, что кто-то так делает — скажите ему, чтобы немедленно прекратил.
Правильный способ — хранить в куке только длинную случайную строку-токен. Эта строка выдаётся юзеру при авторизации и одновременно записывается в базу сайта вместе с идентификатором юзера. Таким образом, по пришедшему токену сайт может определить, какому юзеру этот токен был выдан, в то же время сам по себе по себе токен не содержит никакой информации кроме «шума».
Хранение токенов в базе — важно. Может возникнуть желание заменить токены на подписанные куки. Это желание неправильное, потому что юзер должен иметь возможность инвалидировать некоторые или все свои сохранённые авторизации, например, если он подозревает кражу кук. Хорошим тоном также считается сбрасывать все сохранённые авторизации, когда юзер меняет пароль. Это возможно только если есть единый реестр авторизационных данных, то есть, база сайта.
Ещё один момент. Все говорят, что хранить в базе пароли без хэширования плохо, но почему-то почти никто не говорит того же самого про авторизационные токены. Между тем, с точки зрения безопасности токены практически эквивалентны паролям. Украв (незаметно) базу токенов, злоумышленник сможет войти на сайт под видом любого из пользователей. Поэтому токены и любые подобные им данные необходимо хэшировать при помещении в базу. Хорошая новость состоит в том, что тут, вероятно, не обязательно использовать «тяжёлые» алгоритмы типа bcrypt — токены сами по себе являются длинными случайными строками, и их хэши невозможно взломать брутфорсом, даже если хэш-функция быстрая. Поэтому достаточно использовать любую хэш-функцию, достаточно защищённую от коллизий, например, sha256.
Ну и не лишним будет напомнить, что авторизационные куки должны быть httpOnly — для защиты от XSS.
Ок, поехали дальше. Токен можно украсть, с этим мы ничего сделать не можем. Но мы можем ограничить время его жизни так, чтобы у вора было немного времени, чтобы применить украденный токен. Чем меньше времени валиден токен, тем для нас лучше.
Запишем в базу вместе с токеном время окончания его действия, и если юзер с таким токеном пришёл после этого времени, не считаем его авторизованным. Поскольку мы говорим о постоянной авторизации, то время жизни токена должно составлять хотя бы несколько дней или неделю — это максимальный период, в течение которого юзер может не появляться на сайте и при этом оставаться авторизованным.
Если юзер вернулся на сайт до истечения времени действия токена, то мы узнаём юзера и считаем его авторизованным. При этом мы удаляем старый токен из базы и выдаём юзеру новый случайный токен. Таким образом мы минимизируем время валидности токена, и при частых визитах пользователя на сайт у злоумышленника будет достаточно узкое окно, когда он сможет применить украденную куку.
Проблема в том, что если злоумышленник всё-таки успеет перехватить куку и выпустить для себя новый токен, то юзер при следующем заходе (со старым токеном, уже отменённым) окажется внезапно разлогиненным про непонятной причине. При этом злоумышленник будет продолжать пользоваться сайтом как ни в чём не бывало.
В статье “Improved Persistent Login Cookie Best Practice” предложен красивый способ борьбы с этой проблемой. Дадим юзеру не один, а два токена, в разных куках. Первый работает так как было описано выше, а вот второй мы менять не будем, а наоборот, запишем его в куку с максимальным временем жизни и будем продлевать её при каждом визите. Назовём этот постоянный токен «серией» — потому что, как будет видно, он определяет серию «коротких» токенов. Итак, у юзера есть токен (сменный) и серия (долгоживущая). В базе они тоже будут храниться вместе: Token, Series → UserID.
UPD 16/06/2012 Замечу, что серия тоже в первый раз выдаётся при авторизации юзера, соответственно, один юзер может иметь несколько серий — по числу устройств/браузеров, с которых он заходит на сайт.
Как это работает? Когда юзер приходит на сайт, то сайт ищет в базе комбинацию «серия + токен», и если находит, то юзер считается авторизованным. После этого токен перевыпускается, а серия остаётся прежней.
Теперь предположим, что злоумышленник украл куки (то есть, оба значения — серию и токен). Теперь у двух человек есть одинаковые серия и токен — у злоумышленника и у честного юзера. Когда первый из них приходит на сайт, сайт его узнаёт и перевыпускает токен. Когда на сайт приходит второй, то сайт видит, что серия в базе имеется, но токен неверный. Поскольку токены в пределах одной серии могут идти только последовательно, то эта ситуация трактуется как свидетельство кражи куки — сайт автоматически инвалидирует все токены данной серии и выдаёт пришедшему грозное предупреждение о том, что у него увели куку.
UPD 16/06/2012 При этом серия также удаляется из базы и при следующей авторизации выпускается заново. Иначе злой хакер со своей валидной серией и невалидным токеном сможет постоянно сбрасывать авторизацию у юзера стой же серией.
Таким образом, эта схема позволяет автоматически детектировать кражу кук.
Однако тут есть одна проблема. Предположим, юзер отправляет одновременно несколько запросов к сайту (это может быть и несколько окон с одним сайтом, и ajax, и какие-то динамические ресурсы на странице). Первый обработанный запрос вызовет перевыпуск токена, а вот следующий запрос (напомню, он послан ещё со старым токеном!) сайт сочтёт запросом с украденной кукой и сбросит все авторизации серии. То же самое случится, если у юзера медленный или ненадёжный канал, и послав запрос на сайт он по какой-то причине не получит ответа с новой кукой. Нажав на «релоад», юзер пошлёт запрос со старым токеном, который сайт уже успел инвалидировать — и опять получит сброс авторизации. Поскольку существует модуль Друпала, реализующий эту схему, многие пользователи наблюдали такое поведение на практике (http://drupal.org/node/327263).
Для борьбы с этой проблемой следует считать токены валидными какое-то время после их перевыпуска. Например, если на сервере используется memcached, то при смене токена можно сохранять старый (вместе с серией и userId) в memcache на какое-то малое время (вероятно, единицы минут). Тогда при проверке куки мы сначала смотрим в memcache, и если там есть нужная комбинация — считаем токен ещё валидным. Если нет — то смотрим в базе и далее действуем штатным образом. При этом нужно снова перевыпускать токен на случай если юзер не получил перевыпущенный в прошлый раз.
Мне кажется, это хороший метод, всяко более разумный, чем простое хранение токена в куке с 10-летним expires. А какие методы постоянной авторизации знаете/используете вы?
Способ неплохой, но для некоторых приложений можно обойтись без базы. Берётся symmetrical cipher типа AES256, ключ постоянен и известен только серверу. Составляется сообщение типа salt + user_id + expiration time. Шифруется ключом и отправляется юзеру в виде cookie.
При получении cookie от юзера делается попытка расшифровать её. Если salt верный - считаем, что cookie выдана сервером и верим тому user_id который указан в сообщении.
Это утверждение неверно для описанного мной случая. Когда юзер меняет пароль, вместе со сменой пользователю апдейтится поле, в котором хранится время последней смены пароля. Далее, если приходит cookie, выпущенная ранее этого времени - считаем её недействительной (подразумевается, что issue time этой cookie = expiration time - life time). Цель моего способа - не иметь базу токенов.
Способ, конечно, не претендует на то, чтобы быть полезным для всех случаев. Как я уже выше отметил, "для некоторых приложений можно обойтись без базы".
Опять же, отлов увода куки без базы не сделать.
А вот ездила в Португлию пыталась зайти в гугель-почту, честно ввела парол - не пустили. Вводила несколько раз - наконец-то с 4-го раза предложили послать смс. Попытки прекратила. Подумала, что ошиблась, когда переписывала пароль в блокнот.
Вернувшись домой, получила от гугля два сообщения о том, что какая-то сволочь пыталась взломать мой адрес из Португалии.
У всех этих полезных байд есть одно свойство:
Постоянная авторизация с двух+ устройств становится невозможной.
А для меня, например - это стандартная ституация, из дома - с работы. Или, как мне Седов написал давеча - в течение дня с 3-4компов и ещё с телефона.
Ну, то есть понятно, что можно запоминать браузером для таких целей. Но тогда можно и вообще не париться с постоянкой.
В этом смысле я очень, очень ценю не статьи, а реальный опыт использовования.
Увы, ограничение времени жизни - совершенно дурацкая мера, но риск таки снижает.
А как ещё снижать риск? Только подсовывая в токен какую-то информацию, которую трудно подделать, но такой практически нет.
В большинстве случаев это приемлемо.
Но это ладно. Я же говорил "В большинстве случаев [подразумевая "корпоративного клиента" в основном] это приемлемо.". Т.е. либо айпишник вообще постоянный, либо меняется достаточно редко, чтобы реавторизация по смене айпи считалась "приемлемым неудобством".
На самом деле помимо куков у клиента есть ещё и "сигнатура", "fingerprint" (напр. сочетание User-Agent, Accept, Accept-Language, Accept-Encoding и.ты.ды.). Это тоже можно использовать для усиления защиты. Т.е. осведомлённый хакер и эту "заплатку" обойдёт... если будет осведомлён, конечно же ,-)
ну а middle-man вполне успешно нейтрализуется сертификатом сервера + https.
Хммм... каков полемический задор, однако...
А Вы точно читали те материалы, на которые дали ссылку?
Напоминаю:
-+-
The following user functions must not be reachable through a cookie-based login, but only through the typing of a valid password:
*: Changing the user's password
*: Changing the user's email address (especially if email-based password recovery is used)
*: Any access to the user's address, payment details or financial information
*: Any ability to make a purchase
-+-
Т.е. "постоянная авторизация" неполная. Она, вроде бы, есть, но её, в то же время, и нет (в зависимости от совершаемого действия).
>> Но то что мы называем ? «постоянной авторизацией» всяко не должно слетать при перемещении юзера из одной соты в другую, пока он сёрфит с телефона. Это даже для корпоративных клиентов верно… <<
Для меня это совершенно не очевидно и тем более не очевидно, что такое полиси приемлемо для корпоративных приложений.
Т.е. если это какой-то твитер/чат, то, наверное, это требование логично и приемлемо... но только для ...эммм... сервисов такого рода.
По второму — я это не указывал явно, но имелась в виду авторизация на обычных сервисах, вроде ЖЖ, с соответствующими требованиями к безопасности и доступности. Конечно, для интерфейса банка надо и IP отслеживать и вообще не делать сессию длиннее 10 минут. Но если у Вас будет слетать авторизация в ЖЖ при каждом переподключении к сети, Вы вряд ли обрадуетесь.
На самом деле, тут есть варианты — например, Facebook и Google, похоже, привязывают свои токены к _стране_ (через какой-нибудь geo-ip). Во всяком случае, при поездках за границу у меня их авторизации слетали, а FB так и вовсе запрашивал явное разрешение на вход из нового места.