Доброго времени суток, дорогой читатель. В данной статье я постараюсь рассказать об одном из самых популярных (на сегодняшний день) способов авторизации в различных клиент-серверных приложениях - токен авторизации. А рассматривать мы его будем на примере самой популярной реализации - JSON Web Token или JWT.
Введение
Начнем с того, что важно уметь различать следующие два понятия: аутентификации и авторизации. Именно с помощью этих терминов почти все клиент-серверные приложения основывают разделение прав доступа в своих сервисах.
Очень часто к процессу аутентификации примешивают и процесс индентификации - процесс, позволяющий определить что за пользователь в данный момент хочет пользоваться нашим приложением, например, путем ввода логина. Далее нам, как разработчикам и как ответственным людям хочется убедиться, что данный пользователь действительно тот за кого он себя выдает - и поэтому следует процесс аутентификации, когда пользователь подтверждает, что он тот самый %user_name%, например, путем ввода пароля.
Казалось бы, все что необходимо выполнить для безопасности нашего приложения мы сделали. Но нет, еще одним очень важным шагом в любом клиент-серверном приложении является разграничение прав, разрешение или запрет тех или иных действий данному конкретному аутентифицированному пользователю - процесс авторизации.
Еще раз кратко закрепим: сначала идет идентификация и аутентификация, процессы когда мы определяем и удостоверяемся, что за пользователь в данный момент использует наше приложение, а далее идет авторизация - процесс принятия решения о разрешенных данному пользователю действиях.
Еще одно небольшое введение
Прежде чем начать говорить о самом токене авторизации следует упомянуть для каких целей вообще его решили использовать. Поскольку мы знаем, что почти весь интернет так или иначе построен на протоколе HTTP(или его старшем брате HTTPS) и что он не отслеживает состояние, то есть при каждом запросе HTTP ничего не знает, что происходило до этого, он лишь передает запросы, то возникает следующая проблема: если аутентификация нашего пользователя происходит с помощью логина и пароля, то при любом следующем запросе наше приложение не будет знать все тот же ли этот человек, и поэтому придётся каждый раз заново логиниться. Решением данной проблемы является как раз наш токен, а конкретно его самая популярная реализация - JSON Web Tokens (JWT). Также помимо решения вопросов с аутентификацией токен решает и другую не менее важную проблему авторизации (разграничение разрешенных данному пользователю действий), о том каким образом мы узнаем ниже, когда начнем разбирать структуру токена.
Формальное определение
Приступим наконец к работе самого токена. Как я сказал ранее в качестве токенов наиболее часто рассматривают JSON Web Tokens (JWT) и хотя реализации бывают разные, но токены JWT превратились в некий стандарт, именно поэтому будем рассматривать именно на его примере.
JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON.
Фактически это просто строка символов (закодированная и подписанная определенными алгоритмами) с некоторой структурой, содержащая полезные данные пользователя, например ID, имя, уровень доступа и так далее. И эта строчка передается клиентом приложению при каждом запросе, когда есть необходимость идентифицировать и понять кто прислал этот запрос.
Принцип работы
Рассмотрим принцип работы клиент серверных приложений, работающих с помощью JWT. Первым делом пользователь проходит аутентификацию, конечно же если не делал этого ранее и в этом есть необходимость, а именно, например, вводит свой логин и пароль. Далее приложение выдаст ему 2 токена: access token и refresh token (для чего нужен второй мы обсудим ниже, сейчас речь идет именно об access token). Пользователь тем или иным способом сохраняет его себе, например, в локальном хранилище или в хранилище сессий. Затем, когда пользователь делает запрос к API приложения он добавляет полученный ранее access token. И наконец наше приложение, получив данный запрос с токеном, проверяет что данный токен действительный (об этой проверке, опять же, ниже), вычитывает полезные данные, которые помогут идентифицировать пользователя и проверить, что он имеет право на запрашиваемые ресурсы. Таким нехитрым образом происходит основная логика работы с JSON Web Tokens.
Структура токена
Пришло время обсудить структуру токена и тем самым лучше разобраться в его работе. Первое что следует отметить, что JWT токен состоит из трех частей, разделенных через точку:
Заголовок (header)
Полезные данные (playload)
Подпись (signature)
Рассмотрим каждую часть по подробнее.
Заголовок
Это первая часть токена. Она служит прежде всего для хранения информации о токене, которая должна рассказать о том, как нам прочитать дальнейшие данные, передаваемые JWT. Заголовок представлен в виде JSON объекта, закодированного в Base64-URL Например:
Если раскодировать данную строку получим:
{"alg":"HS256","typ":"JWT"}
Заголовок содержит два главных поля: alg и typ. Поле typ служит для информации о типе токена, но как я уже упоминал ранее, что JWT превратился в некий стандарт, то это поле перестало нести особый смысл и служит скорее для целей будущего, если вдруг появится улучшенная версия алгоритма JWT(2.0), которая заменит JWT. Поле alg задает алгоритм шифрования. Обязательный для поддержки всеми реализациями является алгоритм HMAC с использованием SHA-256, или же, как он обозначен в заголовке, HS256. Для работы с этим алгоритмом нужен один секретный ключ, конкретный механизм работы рассмотрим ниже. Для справки можно также отметить, что существует и асимметричный алгоритм, который можно использовать в JWT, например, RS256. Для работы с ним требуется два ключа - открытый и закрытый. Но в данной статье рассмотрим работу с одним закрытым ключом.
Полезные данные
Перейдем наконец к полезным данным. Опять же - это JSON объект, который для удобства и безопасности передачи представляется строкой, закодированной в base64. Наглядный пример полезных данных (playload) токена может быть представлен следующей строкой:
Что в JSON формате представляет собой:
{"user_id":1,"exp":1581357039}
Именно здесь хранится вся полезная информация. Для данной части нет обязательных полей, из наиболее часто встречаемых можно отметить следующие:
iss - используется для указания приложения, из которого отправляется токен.
user_id - для идентификации пользователя в нашем приложении, кому принадлежит токен.
Одной из самых важных характеристик любого токена является время его жизни, которое может быть задано полем exp. По нему происходит проверка, актуален ли токен еще (что происходит, когда токен перестает быть актуальным можно узнать ниже). Как я уже упоминал, токен может помочь с проблемой авторизации, именно в полезных данных мы можем добавить свои поля, которые будут отражать возможности взаимодействия пользователя с нашим приложением. Например, мы можем добавить поле is_admin или же is_preferUser, где можем указать имеет ли пользователь права на те или иные действия, и при каждом новом запросе с легкостью проверять, не противоречат ли запрашиваемые действия с разрешенными. Ну а что же делать, если попробовать изменить токен и указать, например, что мы являемся администраторами, хотя таковыми никогда не были. Здесь мы плавно можем перейти к третьей и заключительной части нашего JWT.
Подпись
На данный момент мы поняли, что пока токен никак не защищен и не зашифрован, и любой может изменить его и тем самым нарушается вообще весь смысл аутентификации. Эту проблему призвана решить последняя часть токена - а именно сигнатура (подпись). Происходит следующее: наше приложение при прохождении пользователем процедуры подтверждения, что он тот за кого себя выдает, генерирует этот самый токен, определяет поля, которые нужны, записывает туда данные, которые характеризуют данного пользователя, а дальше с помощью заранее выбранного алгоритма (который отмечается в заголовке в поле alg токена), например HMAC-SHA256, и с помощью своего приватного ключа (или некой секретной фразы, которая находится только на серверах приложения) все данные токена подписываются. И затем сформированная подпись добавляется, также в формате base64, в конец токена. Таким образом наш итоговый токен представляет собой закодированную и подписанную строку. И далее при каждом новом запросе к API нашего приложения, сервер с помощью своего секретного ключа сможет проверить эту подпись и тем самым убедиться, что токен не был изменен. Эта проверка представляет собой похожую на подпись операцию, а именно, получив токен при новом запросе, он вынимает заголовок и полезные данные, затем подписывает их своим секретным ключом, и затем идет просто сравнение двух получившихся строк. Таким нехитрым способом, если не скомпроментировать секретный ключ, мы всегда можем знать, что перед нами все еще наш %user_name% с четко отведенными ему правами.
Время жизни токена и Refresh Token
Теперь плавно перейдем к следующему вопросу - времени жизни токена, и сопутствующей этой теме refresh token. Мы помним, что одно из важнейших свойств токена - это время его жизни. И оно совсем недолговечное, а именно 10-30 минут. Может возникнуть вопрос: а зачем такое короткое время жизни, ведь тогда придется каждый раз заново создавать новый токен, а это лишняя нагрузка на приложения. А ответ достаточно очевидный, который включает в себя и одновременно ответ на вопрос: а что делать если токен был перехвачен. Действительно, если токен был перехвачен, то это большая беда, так как злоумышленник получает доступ к приложению от имени нашего %user_name%, но так как access token является короткоживущим, то это происходит лишь на недолгий период. А дальше этот токен уже является не валидным. И именно чтобы обновить и получить новый access token нужен refresh token. Как мы знаем (или если забыли можем снова прочитать в начале) пользователь после процесса аутентификацию получает оба этих токена. И теперь по истечении времени жизни access token мы отсылаем в приложение refresh token и в ответ получаем снова два новых токена, опять же один многоразовый, но ограниченный по времени - токен доступа, а второй одноразовый, но долгоживущий - токен обновления. Время жизни refresh token вполне может измеряться месяцами, что достаточно для активного пользователя, но в случае если и этот токен окажется не валидным, то пользователю следует заново пройти идентификацию и аутентификацию, и он снова получит два токена. И весь механизм работы повторится.
Заключение
В данной статье я постарался подробно рассмотреть работу клиент-серверных приложений с токеном доступа, а конкретно на примере JSON Web Token (JWT). Еще раз хочется отметить с какой сравнительной легкостью, но в тоже время хорошей надежностью, токен позволяет решать проблемы аутентификации и авторизации, что и сделало его таким популярным. Спасибо за уделенное время.
andreymal
Лол нет, самый лёгкий и самый надёжный способ — выдать токен просто из рандомных букв, а в базу положить соответствие между этим токеном и конкретным пользователем. Нужны веские причины, чтобы вместо этого использовать что-то сложное наподобие JWT, и они есть далеко не у каждого проекта.
(А впрочем, есть ещё более лёгкий способ — использовать HTTP Basic аутентификацию и передавать логин-пароль тупо в каждом запросе, но он слишком проблемный и небезопасный и используется редко)
Yuribtr
Ну хорошо. Вы сгенерировали длиннющую рандомную строку, но что делать если у вас много пользователей онлайн, получается что у каждого юзера такая строка должна быть уникальна. Потому как если будут совпадения, то пользователи с протухшими токенами будут внезапно обнаруживать что они зашли от имени другого пользователя. Следовательно вам как то надо учитывать такие сценарии и протухшие токены где-то хранить чтобы случайно не выдать их другому юзеру. По мере роста количества ротаций токенов совпадений будет все больше.
Также непонятно как обновлять протухший токен, если у вас только одна рандомная строка.
Думаю специалисты подробнее вам объяснят, что JWT были придуманы не просто так.
andreymal
Вероятность возникновения совпадения очень близка к нулю. Первое совпадение возникнет через время, превышающее время существования вселенной, даже если генерировать миллионы токенов в секунду (почитайте про UUIDv4 например)
Не надо, потому что см. выше.
Обновлять его до того, как он протухнет, очевидно же.
Да, на Хабре другие специалисты уже неоднократно объясняли, что JWT — это очень нишевая штука, нужная далеко не всем. Почитайте другие хабрапосты про JWT и особенно комментарии к ним.
Yuribtr
Хорошо. Допустим вы генерируете псевдослучайную строку, чтобы быстро авторизоваться (иначе генерация займет приличное время). Но как вы будете противостоять атакам на генератор псевдослучайных чисел?
Злоумышленник же может зарегистрироваться у вас и получить некую последовательность токенов. Другой вопрос, если у вас регистрация закрыта для посторонних, но там другой разговор.
andreymal
/dev/urandom хоть и по сути псевдослучаен, но сделан достаточно умно и инициализируется и периодически обновляется истинно случайными числами — к нему неприменимы атаки на ГСПЧ и его вполне безопасно использовать для генерации любых случайных данных, в том числе связанных с криптографией. (Так по крайней мере в линуксе, но в других ОС тоже есть свои способы безопасной генерации случайных данных)
mayorovp
Очевидно же: используя криптостойкий ГПСЧ, благо таких навалом.
nApoBo3
JWT, по мимо плюсов в виде возможности авторизации, даже без аутентификации, имеет еще один достаточно весомый плюс, он есть практически из коробки, не нужно вот это все городить с базой куда вы пишите ваши случайные строки, с хранением на двух сторонах срока их действия, из коробки у вас система распределенная, в самом токене вы можете передать некоторое кол-во доверенной информации.
При этом ваш вариант, по факту тот же токен, только в качестве источника доверия вы используете базу.
miksir
В PHP вообще сессии в языке из коробки, пользуйтесь, куда уж коробочнее ;)
andreymal
Ну вот как я упомянул в соседнем комментарии — в PHP и Django из коробки именно рандомные строки, а JWT придётся вручную сбоку прикручивать. Авторизация без аутентификации нужна не всем, распределённость — тоже не всем. В общем, это всё не преимущества и не недостатки, а просто особенности, применимость которых нужно оценивать для каждого проекта отдельно
arthuriantech
Представим себе токен, который состоит из timesamp + 36 байт из urandom. Сколько триллионов токенов мы должны генерировать в день, что получить первую коллизию?
FODD
В большнстве фреймворков есть готовые библиотеки для удобной работы с аутентификацией и авторизацией, которые под капотом как-раз JWT и используют. Буквально недавно делали — IdentityServer на бэке, OIDC на фронте. На настройку и внедрение понадобилось около 30 минут с каждой стороны.
Со своими токенами, конечно, более гибко, но «это ведь код писать нужно»
andreymal
Ну, лично я вот использую PHP и Django — у них под капотом как раз рандомная строка, которая кладётся в HttpOnly куки — для веб-приложений с бэкендом-монолитом самое то. (Хотя для нативных приложений куки это немного странно, но тут мне в целом нечего сказать, не моя область)
bogolt
У JWT есть пара преимуществ перед методом хранения строки в бд
— JWT пользователя не нужно хранить в базе вообще. При получении нужно лишь проверить его валидность ( то есть что подпись не повреждена, и что он не истек ) и у вас уже есть ключевая информация о пользователе. Причем так как в жвт можно положить что угодно, хоть кеш часто используемых данных.
— в случае если один и тот же пользователь хочет залогинится из разных мест одновременно в базе придется продумывать более сложную схему хранения уникальных токенов ( так как одному пользоваетелю будет соотвтестовать не ровно один, но множество токенов ). В Jwt об этом думать не нужно, оно сразу работает так.
Из недостатков разве что сложность блокировки утекшено токена, вот тут придется добавлять его в бд, и как-то проверять.
miksir
А еще можно положить в JWT все права доступа и флаг isSuperAdmin… ну что бы вообще не ходить в базу. Ну прям подарок любому хакеру, который рано или поздно доберется до приватного ключа ;)
И конечно же один get из базы сессий — это самая основная нагрузка, которую ваше приложение дает на базу.
bogolt
Зачем ему приватный ключ, если он добрался туда где тот хранится то может уже ходить в базу напрямую, ну или добавить свой ssh ключ в систему и спокойно заходить.
Но это лишний get из базы на каждый пользовательский запрос. Не так уж и мало.
miksir
Далеко не факт. Не факт, что может ходить в базу, не факт, что знает как ходить и может писать везде, не факт, что может работать со всеми внешними апи сайта, там, что бы деньги вывести и много чего еще не факт. Не говоря уже о ssh, которого у веб юзера быть не должно.
А самое смешное, если программер закроет дырку… то хакер с приватным ключом лишь улыбнется. Это в общем к тому, что ключи ротировать нужно, равно как и задумываться о политиках его хранения и доставки на прод… о чем я еще ни одного упоминания в популярных хабр статьях "какой крутой jwt" не видел.
Ради интереса — возьмите все окружающие себя проекты. И посмотрите — сколько и какой длительности запросы в среднем на каждый пользовательский запрос. Уверен, этот миленький get, который еще отлично ложится в быстрые базы, и идеально шардируется, покажется ну… комариным укусом.
Исключения бывают, конечно… но… исключения, и эти ребята там уже своей головой думают — какой им профит принесет jwt, а какую боль.
И, кстати, если мы хотим все же чуть серьезно использовать jwt, то надо задуматься о базе данных отозванных токенов… в которую придется ходить каждый пользовательский запрос ;(
vmkazakoff
У вас секрет для jwt и пароль от root на сервере и пароль от БД одинаковый????
bogolt
Разные. Но если кто-то добрался то того места где хранится приватный jwt то значит и до остальных уже не сложно добраться.
vmkazakoff
Ключ Jwt легче подобрать, имхо. Тем более это можно делать и без вашей системы, т.е. вы даже не узнаете о попытках перебора.
miksir
Ну, ключ jwt подобрать сложно (на грани с невозможно) если не было сделано ошибок. Как минимум с выбором размера ключа. А если у вас еще сделана ротация ключей, то можно забыть про этот вектор атаки.
nApoBo3
Если так рассуждать, то любой хакер и до вашей базы рандомных строк доберется, я бы даже сказал, что ключ защитить проще.
Если допустить, что хакер имеет возможность скомпрометировать токен, то значит он имеет как минимум права вашего веб приложения, а значит он может записать в вашу базу произвольные данные включая случайную строку авторизации.
miksir
Ну, во-первых это разные вектора атак. Раскрытие файла, доступ к базе… а я встречал, что ключ в приложение через env передается, это тоже другой вектор.
Так что даже наличие sql инъекции не значит, что вы можете что-то записать куда хотите. Т.е. банально база с сессиями может быть совершенно другая и не sql. Так что сессию не пропишете. А спертый пароль еще нужно сбрутфорсить. Да и аутентификация под угнанным паролем может мониторится… хотя бы писаться о факте и реальный админ увидит "вы зашли с ip...".
И вообще много что придумать можно. Никто не утверждает, что ключ — это прям ахиллесова пята, а у сайтов с обычными сессиями такой нет. Но факт остается — раскрытие ключа — вполне себе реальная атака, и очень неприятная, особо если вы не задумывались о защите. И последствия такой успешной атаки сильно болезненнее, чем наличие найденной xss или sql injection.
nApoBo3
При чем здесь SQL инекция?
Я понимаю, что это разный вектор атаки, но в данном разрезе это не имеет значения.
Допустим вы спроектировали два приложения, одно использует ключ для подписи токена, другое пишет в базу «случайные» строки.
Чтобы условия были равными, мы принимаем как исходные данные идентичное состояние безопасности сервера и его хотя бы минимально грамотную настройку.
Чтобы утянуть с сервера ключ, вам потребуется получить права минимум приложения которое имеет права этот ключ читать, т.е. права пользователя под которым этот код исполняется.
Если вы получили права пользователя под которым исполняется код, вы сможете подключится к любом хранилищу в который этот код пишет с аналогичными данному коду правами.
Если вы можете подключится к хранилищу с правами кода, значит вы сможете записать в это самое хранилище нужную вам аутентификационную строку без прохождения аутентификации.
Аналогично имея права кода вы можете прочитать все запросы к коду и просто «подсмотреть» логины и пароли в запросах к этому самому коду прилетающих.
Т.е. если злоумышленник имеет возможно компрометации ключа, то ваша система безопасности скомпрометирована в любом случае.
Да, есть один вектор, который не затрагивает атаку на сервер, это утечка ключа не с сервера. Но ровно таким же образом могут утечь и данные админа который может создавать произвольных пользователей. Это опять таки общая уязвимость хранения критических аутентификационных данных.
miksir
Утечка ключа и полноценные права на сервере — все же разные вещи. Не обязательно получить RCE, что бы достать ключ, это может быть просто раскрытие содержимого файла… какой-нибудь сломанный readImage и т.п.
Но основная мысль моя — опасность утечки ключа в том, что эксплуатация его очень незаметна. Сессию можно увидеть в списке сессий пользователя. Вебшел можно найти в рамках сканирования приложения. В случае JWT "из коробки" банальный список сессий сделать — уже проблема, сделать то можно, но это нивелирует плюсы JWT.
nApoBo3
Можно узнать зачем у вас readImage в сервисе аутентификации пользователя?
Так у вас может быть ломанное еще что-то позволяющее писать в базу или читать из нее. Ключ в крайнем случае можно даже на токене хранить и тогда его вообще не возможно будет прочитать.
miksir
Какой сервис аутентификации, о чем вы? JWT сплошь используют программисты, которые знают слова "симфони, бандл, композер интсталл, jwt это круто, экономим запрос в базу". И их учить нужно, а не говорить им про какие-то мифические сервисы аутентификации.
andreymal
Всё верно, но эти преимущества для многих проектов — особенно монолитов — просто не работают: работа проекта может быть бессмысленна без доступа к базе, и от того, что JWT не требует ходить в базу, легче не становится. И да, эта самая блокировка — опять же всё равно придётся в базу ходить. В общем, "средний" проект не получит преимуществ от JWT.
Jek_Rock
А что делать в ситуации когда пользователь сменил пароль и его нужно разлогинить со всех устройств? Пусть даже не со всех, с текущего.
Как только Вы сохраняете JWT в базу он превращается в обычный токен авторизации и теряет свои преимущества.
Или как обновить информацию в токене, если пользователю поменяли права доступа с admin на user, например?
Вот хорошая статья на эту тему cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions
miksir
Тут два подхода. Если мы считаем, что нам достаточно скорости реагирования в несколько десятков минут, то ничего не делаем, а просто ставим время жизни токена соответствующее.
Если для нас нужна реакция быстрее, чем разумно-минимальное время жизни токена, то черный список, да. Это все же отличается от сессий: эта база сильно меньше (там только отозванные токены и то на короткий срок), эта база легко делается децентрализованной — ее легко реплицировать, а можно вообще по шине отдавать изменения или сервисы могут ходить раз в секунду за изменениями.
Т.е. в целом проблемы решаются. Но, тяжело, сложно, накладно… и получается много дешевле плюнуть и юзать сессию с единым сервисом валидации токена.
По-этому и правда, stop using jwt fot session, всячески поддерживаю. Те, кто собирается все же это делать — они должны все просчитать, а не просто начитаться статей в интернете какой jwt крутой.
shuron
Сам владелец токена то не поменялся и в месте с этим его авторизация