Userpic

You are viewing david_m's journal

Давид Мзареулян - Правильная постоянная авторизация

Jun. 15th, 2012

07:45 pm - Правильная постоянная авторизация

Previous Entry Add to Memories Share Track This Flag Next Entry

Сейчас весь интернет увлечённо обсуждает, как правильно хранить пароли. Дело хорошее:) Но я недавно заинтересовался похожей темой и обнаружил, что по ней куда меньше материалов. Тема эта — постоянная авторизация или, проще говоря, функция «Запомнить меня» при входе на сайт.

Всё нижеописанное основано на двух статьях, популярных в англонете, но почему-то в рунете никак не освещённых — это “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. А какие методы постоянной авторизации знаете/используете вы?

Comments:

[User Picture]
From:anight
Date:June 15th, 2012 08:14 pm (local)
Track This
(Link)
> Правильный способ — хранить в куке только длинную случайную строку-токен. Эта строка выдаётся юзеру при авторизации и одновременно записывается в базу сайта вместе с идентификатором юзера. Таким образом, по пришедшему токену сайт может определить, какому юзеру этот токен был выдан, в то же время сам по себе по себе токен не содержит никакой информации кроме «шума».

Способ неплохой, но для некоторых приложений можно обойтись без базы. Берётся symmetrical cipher типа AES256, ключ постоянен и известен только серверу. Составляется сообщение типа salt + user_id + expiration time. Шифруется ключом и отправляется юзеру в виде cookie.
При получении cookie от юзера делается попытка расшифровать её. Если salt верный - считаем, что cookie выдана сервером и верим тому user_id который указан в сообщении.
(Reply) (Thread)
[User Picture]
From:david_m
Date:June 15th, 2012 08:27 pm (local)
Track This
(Link)
Там ниже написано, почему нужна база.
(Reply) (Parent) (Thread)
[User Picture]
From:anight
Date:June 15th, 2012 08:37 pm (local)
Track This
(Link)
> Хорошим тоном также считается сбрасывать все сохранённые авторизации, когда юзер меняет пароль. Это возможно только если есть единый реестр авторизационных данных, то есть, база сайта.

Это утверждение неверно для описанного мной случая. Когда юзер меняет пароль, вместе со сменой пользователю апдейтится поле, в котором хранится время последней смены пароля. Далее, если приходит cookie, выпущенная ранее этого времени - считаем её недействительной (подразумевается, что issue time этой cookie = expiration time - life time). Цель моего способа - не иметь базу токенов.
(Reply) (Parent) (Thread)
[User Picture]
From:david_m
Date:June 15th, 2012 08:47 pm (local)
Track This
(Link)
Зато вместо токенов мы храним время сброса пароля. И в чём профит? Опять же, мы теряем быструю и контролируемую инвалидацию кук. Если юзер НЕ меняет пароль, то раз выпущенную подписанную куку невозможно инвалидировать, она живёт ровно столько, сколько в ней написано.
(Reply) (Parent) (Thread)
[User Picture]
From:anight
Date:June 15th, 2012 09:03 pm (local)
Track This
(Link)
Профит в том, что база юзеров всё равно есть и всё равно объект user, как правило, загружается на каждый запрос. Так что сравнение cookie timestamp с последней разрешённой (не обязательно пароль менять для этого, да) делается без дополнительных расходов.
Способ, конечно, не претендует на то, чтобы быть полезным для всех случаев. Как я уже выше отметил, "для некоторых приложений можно обойтись без базы".
(Reply) (Parent) (Thread)
[User Picture]
From:david_m
Date:June 15th, 2012 09:09 pm (local)
Track This
(Link)
Я как бы не спорю с тем, что для некоторых случаев подписанные куки — самое то. Сессии, например, в них держать можно. Просто этот случай — не из таких, в _этом_ случае подписанная кука с большим временем жизни (а малым его тут делать нельзя) приведёт к дополнительному риску её увода. Тут же суть в том, что токен живёт ровно столько, сколько нужно, и не больше.

Опять же, отлов увода куки без базы не сделать.
(Reply) (Parent) (Thread)
[User Picture]
From:irairopa
Date:June 16th, 2012 12:57 am (local)
Track This
(Link)
а у меня пароли из 16+ знаков.
А вот ездила в Португлию пыталась зайти в гугель-почту, честно ввела парол - не пустили. Вводила несколько раз - наконец-то с 4-го раза предложили послать смс. Попытки прекратила. Подумала, что ошиблась, когда переписывала пароль в блокнот.
Вернувшись домой, получила от гугля два сообщения о том, что какая-то сволочь пыталась взломать мой адрес из Португалии.
(Reply) (Thread)
[User Picture]
From:_pg_
Date:June 16th, 2012 01:52 am (local)
Track This
(Link)
Вероятно, ввод был не в английской раскладке, а в местной, португальской.
(Reply) (Parent) (Thread)
[User Picture]
From:irairopa
Date:June 16th, 2012 02:23 am (local)
Track This
(Link)
сейчас задумалась - вот это точно не проверила
(Reply) (Parent) (Thread)
[User Picture]
From:enternet
Date:June 16th, 2012 03:52 am (local)
Track This
(Link)
Как-то сурово. Как одному пользователю быть залогиненным и дома и на работе?
(Reply) (Thread)
[User Picture]
From:mehanizator
Date:June 16th, 2012 11:23 am (local)
Track This
(Link)
ну а что мешает иметь несколько авторизаций, у каждой своя серия.
(Reply) (Parent) (Thread)
[User Picture]
From:david_m
Date:June 16th, 2012 12:04 pm (local)
Track This
(Link)
А в чём проблема? Дома одна серия, на работе другая.
(Reply) (Parent) (Thread)
[User Picture]
From:romikchef
Date:June 16th, 2012 10:28 am (local)
Track This
(Link)
Погодь.
У всех этих полезных байд есть одно свойство:
Постоянная авторизация с двух+ устройств становится невозможной.
А для меня, например - это стандартная ституация, из дома - с работы. Или, как мне Седов написал давеча - в течение дня с 3-4компов и ещё с телефона.

Ну, то есть понятно, что можно запоминать браузером для таких целей. Но тогда можно и вообще не париться с постоянкой.

В этом смысле я очень, очень ценю не статьи, а реальный опыт использовования.
(Reply) (Thread)
[User Picture]
From:david_m
Date:June 16th, 2012 12:10 pm (local)
Track This
(Link)
Я, видимо, плохо написал, сейчас проапдейтил. Серия выдаётся при авторизации по паролю, и она на каждом устройстве своя.
(Reply) (Parent) (Thread)
[User Picture]
From:1master
Date:June 17th, 2012 09:14 am (local)
Track This
(Link)
Все это будет работать, пока тебя конкретно не пытаются поломать. Поскольку серия столь же спокойно крадется, как и токен.

Увы, ограничение времени жизни - совершенно дурацкая мера, но риск таки снижает.
(Reply) (Thread)
[User Picture]
From:david_m
Date:June 17th, 2012 11:19 am (local)
Track This
(Link)
Конечно крадётся. В общем, она для этого и делается…

А как ещё снижать риск? Только подсовывая в токен какую-то информацию, которую трудно подделать, но такой практически нет.
(Reply) (Parent) (Thread)
[User Picture]
From:1master
Date:June 17th, 2012 12:47 pm (local)
Track This
(Link)
Ну еще можно двухфакторные схемы использовать, но это адский геморрой от слова совсем.
(Reply) (Parent) (Thread)
[User Picture]
From:david_m
Date:June 17th, 2012 12:52 pm (local)
Track This
(Link)
Двухфакторные — это когда ты юзера авторизуешь. А когда он уже авторизован и сайт его должен узнать «прозрачным» образом — тут ничего двухфакторного уже не сделать.
(Reply) (Parent) (Thread)
[User Picture]
From:1master
Date:June 18th, 2012 12:32 am (local)
Track This
(Link)
Почему не сделаешь? Запросто сделаешь. Аппаратное хранилище, которое отдает токен только нужному сайту.
(Reply) (Parent) (Thread)
[User Picture]
From:david_m
Date:June 18th, 2012 12:51 am (local)
Track This
(Link)
А-а… у-у… нет, это слишком много секса:)
(Reply) (Parent) (Thread)
[User Picture]
From:slowkukuing
Date:July 5th, 2012 07:49 pm (local)
Track This
(Link)
есть ещё одна ступень защиты - привязка токена/серии к IP.
В большинстве случаев это приемлемо.
(Reply) (Thread)
[User Picture]
From:david_m
Date:July 5th, 2012 08:15 pm (local)
Track This
(Link)
_Постоянную_ авторизацию нельзя привязывать к IP.
(Reply) (Parent) (Thread)
[User Picture]
From:slowkukuing
Date:July 6th, 2012 08:36 pm (local)
Track This
(Link)
Постоянная авторизация, вообще-то, это миф. Во всяком случае она выполняется на лизовой основе (как DHCP, например) и "по законам жанра" периодически должна перепроверяться (требования безопасности).

Но это ладно. Я же говорил "В большинстве случаев [подразумевая "корпоративного клиента" в основном] это приемлемо.". Т.е. либо айпишник вообще постоянный, либо меняется достаточно редко, чтобы реавторизация по смене айпи считалась "приемлемым неудобством".

На самом деле помимо куков у клиента есть ещё и "сигнатура", "fingerprint" (напр. сочетание User-Agent, Accept, Accept-Language, Accept-Encoding и.ты.ды.). Это тоже можно использовать для усиления защиты. Т.е. осведомлённый хакер и эту "заплатку" обойдёт... если будет осведомлён, конечно же ,-)

ну а middle-man вполне успешно нейтрализуется сертификатом сервера + https.
(Reply) (Parent) (Thread)
[User Picture]
From:david_m
Date:July 6th, 2012 09:11 pm (local)
Track This
(Link)
Ну, совсем постоянная — до тепловой смерти Вселенной — авторизация, конечно, миф. Но то что мы называем − «постоянной авторизацией» всяко не должно слетать при перемещении юзера из одной соты в другую, пока он сёрфит с телефона. Это даже для корпоративных клиентов верно…

(Reply) (Parent) (Thread)
[User Picture]
From:slowkukuing
Date:July 7th, 2012 12:12 am (local)
Track This
(Link)
>> Ну, совсем постоянная — до тепловой смерти Вселенной — авторизация, конечно, миф <<
Хммм... каков полемический задор, однако...
А Вы точно читали те материалы, на которые дали ссылку?
Напоминаю:
-+-
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
-+-
Т.е. "постоянная авторизация" неполная. Она, вроде бы, есть, но её, в то же время, и нет (в зависимости от совершаемого действия).


>> Но то что мы называем ? «постоянной авторизацией» всяко не должно слетать при перемещении юзера из одной соты в другую, пока он сёрфит с телефона. Это даже для корпоративных клиентов верно… <<
Для меня это совершенно не очевидно и тем более не очевидно, что такое полиси приемлемо для корпоративных приложений.
Т.е. если это какой-то твитер/чат, то, наверное, это требование логично и приемлемо... но только для ...эммм... сервисов такого рода.
(Reply) (Parent) (Thread)
[User Picture]
From:david_m
Date:July 7th, 2012 12:26 am (local)
Track This
(Link)
Про ограничения прав замечание верное, но оно (ограничение) всё-таки несколько в стороне от вопроса хранения кук.

По второму — я это не указывал явно, но имелась в виду авторизация на обычных сервисах, вроде ЖЖ, с соответствующими требованиями к безопасности и доступности. Конечно, для интерфейса банка надо и IP отслеживать и вообще не делать сессию длиннее 10 минут. Но если у Вас будет слетать авторизация в ЖЖ при каждом переподключении к сети, Вы вряд ли обрадуетесь.

На самом деле, тут есть варианты — например, Facebook и Google, похоже, привязывают свои токены к _стране_ (через какой-нибудь geo-ip). Во всяком случае, при поездках за границу у меня их авторизации слетали, а FB так и вовсе запрашивал явное разрешение на вход из нового места.
(Reply) (Parent) (Thread)