Безопасная аутентификация: как работать с токенами

Введение

Практические каждый разработчик сталкивался с авторизацией пользователей. Если на вашем сайте есть понятие пользователь, то скорей всего есть и какая-то процедура их регистрации и входа на сайт. И большинство разработчиков знают, как это делается. Пользователь ввел логин и пароль, вы отправили их на сервер, сервер дает токен, вы его затем используете для всех остальных запросов. Просто и понятно, можем закругляться и расходиться!

Вот только вы уверены, что в какой-то момент времени кто-то не найдет способ, как украсть аккаунты других пользователей? Что кто-то не сможет найти способ, как подделать свои права в системе? И ладно если у вас нет ничего критичного на сайте и злоумышленникам он не интересен. А если какой-то важный корпоративный продукт? Если имеется какой-то платежный функционал? Хранятся какие-то конфиденциальные данные? Вот тогда не подумав о защите заранее, потом будет очень больно. Давайте разберемся шаг за шагом, что же можно сделать в этой ситуации, и как защитить себя заранее.

Примечание

Т.к. большинство документаций фреймворков приводят множество примеров, как авторизовать пользователя и выпустить токен, есть готовые решения вроде Passport библиотек, и до этого момента должно быть все понятно, то мы разберем именно то, что происходит после этого. Пользователь передал логин и пароль, вы выпустили токены access и refresh. Что дальше?

Мы разберем сценарий, когда ваша система выпускает пару токенов - access и refresh. Один для авторизации на разных запросах, а другой служит для обновления токенов.

Этапы

Сохранение токенов

Некоторые разработчики на запрос входа отправляют токены прямо в теле ответа - frontend как-нибудь с ними разберется, это его проблема. Вот только способы сохранения, которые доступны в браузере, не совсем безопасны. Если JS может записать токен, то он может его и прочитать. И здравствуй XSS уязвимость. При особом таланте злоумышленника, он может найти способы как украсть это значение.

Сохранять токены желательно так, чтобы к ним и доступа то не было. И хорошим вариантом тут будут Cookies. И не простые печеньки, а золотые со специальными флагами защиты: HttpOnly, Secure и SameSite.

  • HttpOnly - запрещает доступ к Cookie из JavaScript. Только сервер может читать и записывать их. Что уже звучит как немного безопаснее.
  • Secure - запрещает передачу Cookie по незащищенному соединению. Только по HTTPS, а это обязательное шифрование, что тоже хорошо.
  • SameSote - запрещает передачу Cookie на другие сайты. Только на тот, который их и установил. Это защита от CSRF атак.

Уже хорошее комбо. Вот только теперь Cookies с токенами шлются при каждом запросе. И насчет access токена все и правильно, так и должно быть. А вот зачем слать refresh на каждый запрос? Тогда и смысл двух токенов теряется, если оба всегда работают синхронно. И отменить отправку нельзя, раз JS ничего контролировать не может. А точно ли нельзя?

Ведь если подумать, то refresh токен нужен для обновления access токена, когда у того истечет срок жизни. В подавляющем большинстве проектов за это задачу ответственен конкретный метод API, который выполняет задачу обновления токена. У него URL будет примерно /api/auth/refresh, или иной вариант соответсвующий вашему проекту. И вот прекрасная находка - Cookie можно настроить так, чтобы они передавались только на нужных маршрутах. За это ответственен параметр path которому вполне можно поставить ограничение на маршрут /api/auth/refresh. Ну и бонусом стоит обновлять и refresh токен после его использования, чтобы еще более уменьшить риски.

Отличненько! Теперь access токен шлется на все запросы, refresh только при обновлении токенов, украсть теперь становится их намного сложнее, а значит система уже стала чуточку безопаснее. Но не остановимся на достигнутом...

Генерация токенов

Сделаем шаг назад (ну или вперед, если говорить о процессе уже обновления токенов) и поговорим о том, как токены выпускаются. Как правило, это JWT алгоритм, который выпускает вам токены, предварительно вшив в них какие-то данные и подписав их с использованием соли, которую знает только сервер. Что может пойти не так?

Если соль (он же секрет токена) будет каким-то образом украден, то в любой момент можно будет подделать любые токены любых пользователей. Как-то не хорошо получается. Что же тут можно придумать?

Ну, во-первых, можно использовать разную соль для разных типов токенов - access и refresh. Т.к. refresh токен используется реже, то и риск угрозы при таком подходе становится меньше. Следующим этапом можно сделать соль разную для разных пользователей. Тогда узнав одно значение, не получится навредить более чем одному пользователю. Но тут стоит не забывать о балансе скорости и безопасности. Если чтение секрета токена из места его хранения (например, база данных), имеет задержку, то возрастает нагрузка на систему, и страдает безопасность. Компромиссом может стать использование разных секретов для refresh токена, но одного для access токена. Это обеспечит баланс скорости и безопасности.

Т.к. access токены живут не долго, то, при необходимости, можно изменить его секрет в любой момент и не важно, что у всех пользователей сбросится access токен - выполнится запрос на обновление токенов и все будет хорошо. Для refresh токенов можно хранить соль в базе данных уникальную для каждого пользователя. Т.к. чтение будет происходить только при обновлении токенов, то слишком большой нагрузки на сервер не будет. Да и отозвать все токены одного пользователя станет проще.

Срок жизни токенов

Выше все говорится о том, что access токены живут не долго, а refresh дольше и он ответственен за обновление. При реализации описанного выше подхода, все это приобретает смысл. Теперь просто и удобно назначить access токену короткий срок жизни, например, минут 30. А refresh токен сделать более живучим - день или неделя. Если пользователь вернется на сайт в течении этого срока, то все будет хорошо, его авторизация обновится и он этого даже не заметит. Если дольше - то и безопаснее его выкидывать из системы.

Вы можете поиграться со сроками и определить их такими, чтобы это удовлетворяло вашим потребностям в уровне безопасности и комфорте пользователей.

Ограничить срок жизни токенов стоит в нескольких местах. На стороне браузера, т.е. Cookies, должны иметь соответствующий параметр срока жизни - по истечению браузер его автоматически очистит. Сам же токен при выпуске тоже должен иметь параметр срока истечения его. По завершению срока его просто не примет JWT алгоритм. Дополнительно вы можете контролировать наличие токенов и срок их жизни, например, используя Redis или ему подобные инструменты. Там тоже можно устанавливать срок хранения записи. А бонусом вы получите дополнительную защиту, что никто не сможет выпустить поддельный токен, т.к. его просто не будет в вашем хранилище. И отзывать токены станет легко и просто.

Дополнительные меры защиты

Сделав все вышеперечисленное, вы уже поднимите безопасность вашего продукта на новый уровень. Но если ваша система того требует, то можно поднимать защиту все больше и больше. Можно связывать токены с конкретными устройствами или IP адресами. Тогда при попытке использовать токен с иного устройства, он не будет принят вашим сервером. Можно использовать более сложные алгоритмы выпуска и проверки.

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

Безопасная аутентификация: как работать с токенами