Что такое токен авторизации
Перейти к содержимому

Что такое токен авторизации

  • автор:

JWT — как безопасный способ аутентификации и передачи данных

JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.

В простом понимании — это строка в специальном формате, которая содержит данные, например, ID и имя зарегистрированного пользователя. Она передается при каждом запросе на сервер, когда необходимо идентифицировать и понять, кто прислал этот запрос.

В этой статье разберу, что такое Access токен, Refresh токен и как с ними работать.

Для дальнейших разборов будет использован токен:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9.E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0

После того, как посетитель прошел авторизацию в нашей системе, указав свой логин и пароль, система выдает ему 2 токена: access token и refresh токен.

После чего посетитель, когда хочет получить с сервера данные, например, свой профиль, вместе с запросом он передает Access токен, как на примере выше. Сервер, получив его проверяет, что он действительный (об этом чуть ниже), вычитывает полезные данные из него (тот же user_id) и, таким образом, может идентифицировать пользователя.

Токен разделен на три основные группы: заголовок, полезные данные и сигнатура, разделенные между собой точкой.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 — это первая часть токена — есть заголовок. Она закодирована в Base64 и если её раскодировать, получим строку:

Это можно проверить прям в браузере, выполнив в консоле или js коде:

typ — это наш тип токена JWT. Alg — алгоритм шифрования HMAC-SHA256. Их может быть несколько, но здесь буду говорить именно об этом алгоритме.

Вторым блоком идет eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9

Это есть полезные данные, так же закодированные в Base64. После раскодирования получим:

Данные могут быть любыми. Главное, чтобы по ним можно было идентифицировать пользователя. В нашем случае — это user_id и exp — время окончания действия текущего токена.

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

Последняя часть токена — наиболее важная. У нас это E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0

Как вы уже могли заметить — первые данные передаются практически в открытом виде и раскодировать их может любой. Но шифровать их нет необходимости. Цель токена — подтвердить, что эти данные не были изменены. Вот для этих целей и выступает сигнатура. И чтобы её сгенерировать нужен приватный ключ. Ну или некая секретная фраза, которая находится только на сервере. Только с помощью этого ключа мы можем создать сигнатуру и проверить, что она была создана именно с помощью его.

Она получается примерно следующим образом:

Берем заголовок, например <"alg":"HS256","typ":"JWT">и кодируем его в base64, получаем ту самую часть eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Тоже самое проделываем с данными eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9

После этого склеиваем их и получаем eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9

Далее эти данные шифруем с помощью нашего алгоритма HMAC-SHA256 и ключа.

Для проверка токена необходимо проделать ту же операцию.

Берем склейку заголовок + данные, кодируем с помощью алгоритма HMAC-SHA256 и нашего приватного ключа. А далее берем сигнатуру с токена и сверяем с результатом кодирования. Если результаты совпадают — значит данные подтверждены и можно быть уверенным, что они не были подменены.

Основной токен, про который шла речь выше, обычно имеет короткий срок жизни — 15-30 минут. Больше давать не стоит.

Как только время выйдет, пользователю снова придется проходить авторизацию. Так вот чтобы этого избежать, существует Refresh токен. С помощью него можно продлить Access токен.

В действительности, Refresh токен обязательно должен быть одноразовым. Его задача — получить новую пару токенов. Как только это было сделано, предыдущий токен будет считаться недействительным. Срок жизни Refresh токена уже может быть большим — до года, а может даже и больше.

У него, обычно, нет какой-то структуры и это может быть некая случайная строка.

Для проекта odo24.ru я использовал следующий подход.

Генерируется Access токен и после случайная строка, например T6cjEbghMZmybUd_fhE

С нашего нового Access токена eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9.E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0 беру последние шесть знаков, получаю Ed4wF0

Склеиваю и получаю рефреш токен T6cjEbghMZmybUd_fhEEd4wF0

Это сделано для привязки Access токена к Refresh. Для получения новых токенов необходимо передать эти два токена. Делается проверка на их связку и только после валидируется Access токен. Если и второй этап прошел успешно, тогда получаем с базы данных по текущему user_id рефреш токен и сверяем с тем, что к нам пришел. Если они совпадают, тогда генерируются новые токены и в базе данных обновляется Refresh токен на новый.

В моем случае я разделил оба токена и храню в разных местах. Access токен нужен только для идентификации пользователя и на клиенте (JS) он не нужен, поэтому он передается в Cookie (http only).

Refresh токен хранится в LocalStorage и используется только когда Access токен перестал быть актуальным.

Представим ситуацию, когда у нас каким-то образом украли Access токен. Да, это уже плохо и где-то у нас брешь в безопасности. Злоумышленник в этом случае сможет им воспользоваться не более чем на 15-30 минут. После чего токен «протухнет» и перестанет быть актуальным. Ведь нужен второй токен для продления.

Если украли Refresh токен, то без Access токена (который недоступен в JS) продлить ничего нельзя и он оказывается просто бесполезным.

Самая неприятная ситуация — это когда удалось увести сразу 2 токена. В этом случае злоумышленник сможет пользоваться системой неограниченное время. Точнее когда пользователь попытается войти в систему, его не пустит, т.к. его Refresh токен уже будет неактуальным, и ему придется вводить логин и пароль. Только в этом случае злоумышленник потеряет контроль над чужой учетной записью.

В своей реализации Refresh токена использовал общую длину 24 знака. Первые 6 знаков — это дата его «протухания», следующие 12 знаков — случайно сгенерированные данные. И в конце 6 знаков — это часть Access токена последней части сигнатуры.

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

Дата содержит год, месяц, день, час и минуты. Хранится в ASCII

Кодирование даты на Golang:

Всю реализацию на Go можно изучить на Github-е

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

Неприметные токены. Часть 1. Теория

В ходе тестирования на проникновение нередко удается получить доступ с правами уровня локального администратора к какому-то сетевому объекту, функционирующему под управлением операционной системы семейства Windows.

Следующим этапом, как правило, является повышение привилегий до администратора домена. Существует множество способов пост эксплуатации конечной системы при наличии к ней административного доступа. Наиболее перспективным методом представляется поиск учетных данных, таких как:

  • пароли в открытом виде
  • NT-хэши паролей
  • TGT
  • MsCache v2 хеши
  • секреты в DPAPI и др.

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

Первая часть содержит теорию необходимую для дальнейшего понимания материала.

Сессия пользователя#

Как обычно, основательно подойдем к изучению вопроса и начнем издалека. Прежде чем приступать к токенам необходимо разобраться с таким понятием, как сессия пользователя.

Бытовая аналогия#

Рассмотрим следующий пример — организация доступа на стадионы чемпионата мира по футболу 2018.

Поверхностно опишем процедуру прохода:

  1. Каждый, кому требовалось пройти на стадион, сначала приходил в один из нескольких аккредитационных центров, где предоставлял свой паспорт и обоснование для прохода.
  2. Сотрудник аккредитационного центра проверял личность предъявителя и его обоснование, тем самым аутентифицируя его. Если личность была болельщиком, то обоснованием являлся билет на матч, если репортером, то требовалось наличие соответствующей записи в заранее согласованной базе данных. Кроме того требовалось смотреть в базу, чтобы выявлять нежелательных посетителей, входящих в черный список (пранкеры, агрессивные фанаты). Отметим, что процедуру проверки в базе данных можно считать довольно ресурсозатратной.
  3. В результате успешной проверки посетителю стадиона выдавалась аккредитация. Таким образом осуществлялась авторизация.

Каждая аккредитация содержала следующую информацию:

  • ФИО и фото владельца (замазаны березовым цветом)
  • Перечень стадионов доступных для посещения
  • Роль владельца (волонтер, обслуживающий персонал, сотрудник безопасности, журналист и т.д.)
  • Перечень зон внутри стадиона, доступных для посещения (трибуны, раздевалки, подтрибунные помещения, микс зона и т.д.)

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

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

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

Сессия в Windows#

Рассмотрим, что происходит при входе пользователя в Windows:

  1. Пользователь садится за компьютер и предоставляет системе свои аутентификационные данные (логин, пароль, название домена, smart карту или другие).
  2. Система передает полученную информацию в свой локальный центр безопасности (далее — LSA).
  3. LSA осуществляет аутентификацию пользователя.

Как именно это происходит выходит за рамки настоящего материала. Для желающих ознакомиться с тонкостями процедуры аутентификации ранее были написаны другие материалы, где рассматриваются принципы работы протокола Kerberos.

  1. В результате успешной аутентификации пользователю предоставляется сессия, позволяющая получать доступ к защищаемым объектам операционной системы при наличии соответствующих прав.

В Windows в качестве защищаемых объектов выступают совершенно различные объекты, например: файлы, именованные каналы, ключи реестра, процессы, потоки, сетевые папки, службы, устройства, объекты Active Directory и т.д.

Пользователю не требуется заново вводить свой пароль при каждом открытии файла или запуске прикладной программы. Это называется концепцией единого входа (Single Sign On — SSO), которая реализуется, в том числе с помощью механизма сессий.

В рамках сессии для получения доступа к защищаемому объекту пользователю необходимо обладать определенным контекстом безопасности.

Связь пользователя, сессии и контекста безопасности

Контекст безопасности представляет собой набор специальных атрибутов необходимых операционной системе для принятия решений о предоставлении пользователю доступа к защищаемому объекту или разрешении выполнить системную операцию.

Под системной операцией понимается действие, требующее наличия определенной привилегии для его выполнения, например выключение системы или изменение значения счетчика времени.

Важно отметить, что по сути контекст безопасности является своеобразной “аккредитацией” пользователя. Всякий раз, когда пользователь обращается к защищаемому объекту, система смотрит сессию пользователя и проверяет связанную с ней “аккредитацию”.

Процессы в Windows#

Пользователь использует систему для выполнения различных программ. Программа представляет собой некоторый код (набор команд и инструкций), который хранится в системе в виде файлов. Программа используется в рамках процесса.

Процесс — это объект операционной системы, содержащий набор ресурсов (в частности выделенный диапазон адресов виртуальной памяти) и данных используемых в ходе работы программы.

Уместно провести следующую аналогию:

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

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

Токены в первом приближении#

Что происходит, когда пользователь открывает файл в текстовом редакторе?

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

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

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

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

Первоначально токен создается в ходе авторизации пользователя, на одном из последних этапов входа в систему. Указанный токен присваивается первому процессу, созданному для работы от имени авторизованного пользователя.

Важно понимать, что процесс, не имеющий токена, не может существовать. В дальнейшем новые процессы по умолчанию наследуют токен доступа от родительского процесса.

Отметим, что создать токен “вручную” нельзя. Прямой доступ к отдельным атрибутам токенов из пользовательского режима невозможен. Это сделано, во избежание несанкционированного изменения полномочий пользователя путем редактирования токенов. Только программный код, выполняющийся в режиме ядра, может получить прямой доступ к атрибутам токена (равно как и к любым другим объектам операционной системы).

Чуть более подробно о локальном повышении привилегий в результате эксплуатации критической уязвимости с последующим выполнением кода в пространстве ядра и манипуляцией токеном доступа можно почитать здесь.

Таким образом между пользователем, его сессией, процессами и токенами имеется следующая связь:

Теперь в первом приближении можно ввести следующее определение:

Токен доступа пользователя — объект ядра ОС Windows, описывающий контекст безопасности процесса, работающего от имени указанного пользователя.

Синонимы: токен, токен доступа, маркер доступа, маркер безопасности.

Атрибуты токена#

Перечислим некоторые основные атрибуты, содержащиеся в токене доступа пользователя:

  • Идентификатор пользователя
  • Идентификатор входа в систему (Logon ID — LUID)
  • Перечень локальных и доменных групп, в которые входит пользователь
  • Список привилегий, которыми обладает пользователь

Пример отображения некоторых атрибутов токена

Более наглядно посмотреть токены и их атрибуты можно с помощью утилиты TokenViewer

Разумеется атрибутов гораздо больше. Для первого знакомства приведенного списка достаточно. Далее постепенно рассмотрим другие важные атрибуты, а также различные виды токенов доступа.

Уровни целостности#

Начиная с Windows Vista, в операционных системах семейства Windows реализован механизм мандатного контроля целостности (mandatory integrity control, MIC).

Согласно MIC в Windows каждому защищаемому объекту операционной системы присваивается мандатная метка, соответствующая определенному уровню целостности. Физически мандатная метка представляет собой целое число. Чем больше число, тем выше уровень целостности.

Уровень целостности процесса хранится в токене указанного процесса в списке групп:

В современных версиях Windows поддерживаются следующие уровни целостности:

Числовой идентификатор Название Описание
0 Untrusted Используется редко, например в ходе анонимного подключения.
4096 Low Назначается процессам браузеров или контейнеров, чтобы ограничить права на запись в отношения системных файлов или ключей реестра.
8192 Medium По умолчанию назначается большинству процессов прикладных программ.
8448 Medium Plus Недокументирован.
12288 High Назначается процессам прикладных программ администратором.
16384 System Автоматически назначается всем системным процессам. Процессам прикладных программ назначаться не может.
20480 Protected Process Назначается специальным защищаемыми процессам

Основная идея мандатного контроля целостности заключается в том, что “низкоцелостные” субъекты не могут изменять “высокоцелостные” объекты.

Рассмотрим, как это используется на практике.

Токены привилегированных учетных записей#

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

  • Builtin Administrators
  • Domain / Enterprise Administrators
  • Policy Administrators
  • Account Operators
  • Backup Operators

а также на предмет наличия следующих “опасных” привилегий:

  • SeBackupPrivilege
  • SeCreateTokenPrivilege
  • SeDebugPrivilege
  • SeImpersonatePrivilege
  • SeLabelPrivilege
  • SeLoadDriverPrivilege
  • SeRestorePrivilege
  • SeTakeOwnershipPrivilege
  • SeTcbPrivilege

Если учетная запись пользователя состоит в одной из подобных групп или обладает хотя бы одной из перечисленных привилегий, то по умолчанию LSA создает два раздельных токена:

  1. Полный токен (в англ. “elevated token”), содержащий все назначенные права, группы, привилегии и имеющий высокий уровень целостности.
  2. Отфильтрованный токен, в частности обладающий следующими ограничениями:
    • отсутствуют “опасные” привилегии
    • присваивается средний уровень целостности

Сравнение логона администратора и обычного пользователя

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

Содержимое токена процесса администратора, запущенного в обычном режиме

Когда пользователю необходимо выполнить задачу, требующую повышенного токена доступа, Windows автоматически запрашивает подтверждение. Этот запрос называется запросом на повышение прав и реализуется с помощью специального компонента — UAC (User Account Control).

Пример оконных сообщений UAC

Содержимое токена процесса, запущенного от имени администратора

Более детальное описание механизма UAC выходит за рамки настоящего материала.

Виды логонов#

Вход в Windows можно осуществить по-разному. Для простоты можно выделить следующие два типа входа (строго говоря их больше):

  • Интерактивный
  • Сетевой (неинтерактивный)

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

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

Просмотр содержимого сетевой папки

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

Если сессия пользователя, в рамках которой запрашивается доступ к сетевой папке, создавалась с помощью интерактивного входа, то используемые для входа учетные данные пользователя были сохранены в LSASS. В этом случае система автоматически использует сохраненные учетные данные пользователя (NT хеш или TGT) для аутентификации на удаленной машине.

Это еще один из примеров, иллюстрирующий поддержку принципа единого входа в Windows.

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

Связь между токенами, различными видами сессий и учетными данными

Еще раз отдельно отметим:

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

Теперь рассмотрим, что происходит с другой стороны в удаленной системе.

Сетевая папка по сути является сервисом, предоставляемым некоторым SMB сервером. Процесс “SMB сервера” обрабатывает запрос на аутентификацию и передает полученные учетные данные в LSA, где проверяется их подлинность. Самое интересное, что происходит в результате успешной аутентификации пользователя, запрашивающего просмотр содержимого сетевой папки.

Процесс “SMB сервера” работает от имени и с правами псевдопользователя SYSTEM. Предоставление прав системы всякому аутентифицированному к SMB серверу пользователю однозначно избыточно. Более того указанный пользователь может обладать доступом не ко всему содержимому сетевой папки.

То есть, процессу “SMB сервера” было бы очень удобно как-то работать от имени своих клиентов. В Windows такой механизм предусмотрен и называется олицетворением (англ. Impersonation).

Олицетворение#

До этого уже немного была затронута тема процессов в операционной системе Windows. Теперь расширим это понятие. У каждого процесса есть как минимум один поток, выделяемый при создании процесса (как правило, на практике потоков больше). Этот поток может породить другие потоки, те в свою очередь новые и т.д.

Рассматриваемый всюду ранее токен доступа, назначаемый процессу операционной системы при его создании, называется первоначальным токеном или токеном процесса (англ. primary token).

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

Токен потока, применяемый для заимствования контекста безопасности другого пользователя, называется олицетворяющим токеном (англ. impersonation token).

Таким образом появляется еще одна классификация токена: первоначальный или олицетворяющий.

В связи изложенным можно уточнить определение токена:

Токен доступа пользователя — объект ядра ОС Windows, описывающий контекст безопасности процессов или потоков, работающих от имени указанного пользователя.

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

Выдача олицетворяющего токена при сетевом входе

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

Аутентификации при сетевом входе

Еще раз обратим внимание, что токен доступа не содержит в себе никаких учетных данных пользователя. По сути токен используется для идентификации пользователя, так как в нем хранится информация о группах и привилегиях. На основе этой информации принимается решение о предоставлении доступа к защищаемым объектам. Вспомните аналогию с аккредитационной картой. Аккредитация не аутентифицирует, но дает возможность быстро понять куда разрешен доступ.

Уровни олицетворения#

Олицетворяющие токены имеют следующую важную характеристику — уровень олицетворения, который отражает степень того, насколько сервер может олицетворять клиента. Всего существуют следующие четыре уровня олицетворения:

Название Описание
Anonymous Удаленный сервер не может идентифицировать или олицетворять клиента. Используется для анонимного подключения.
Identification Удаленный сервер может идентифицировать, но не может олицетворять клиента.
Impersonation Удаленный сервер может идентифицировать и олицетворять клиента локально в системе. Подобные токены обычно создаются в результате сетевого входа, например при доступе к FTP серверу.
Delegation Удаленный сервер может идентифицировать и олицетворять клиента в удаленных системах. Подобные токены обычно создаются в результате интерактивного входа, например по RDP.

Классификации токенов#

Обобщая написанное выше, получается, что токен:

  1. Содержит перечень групп и привилегий пользователя
  2. Может быть полным, а может быть фильтрованным
  3. Может быть связан с интерактивной или неинтерактивной сессией, то есть иметь связь с учетными данными или не иметь
  4. Может быть первоначальным, а может быть олицетворяющим. Если токен олицетворяющий, то он обладает одним из четырех уровней олицетворения

Заключение#

В настоящей статье было рассмотрено устройство токенов доступа в Windows. Был представлен обзор основных атрибутов по которым можно классифицировать токены, а также рассказано о связи токенов, сессий и аутентификационных данных. Изложенные сведения полезны для понимания атак, связанных с манипуляциями токенами.

Token-Based Authentication

Tyroo West

In this tutorial, we will cover token-based authentication .

The general concept behind token-based authentication is the ability for users to obtain a token in exchange for their user credentials. This token authorizes the user to access protected resources without the need for querying for credentials.

Once the token is obtained, the user is now authenticated and can access protected resources for a specified amount of time.

In other words…

Typically when you check into a hotel you are given a room key. Think of the room key as your token. This room key gives you the user, access to the protected resources, such as the gym and the pool.

You are authorized to use the gym and pool for a specified amount of time: the duration of your staty at the hotel. Once you have checked out of the hotel, you are no longer authorized to access those protected resources.

What is a cookie? ��

Cookies are small pieces of data sent from a website and are stored in the user’s web browser while the user is browsing that website. Every time the user loads that website back, the browser sends that stored data back to website or server, to distinguish user’s previous activity.

G etting Started

In this tutorial, the emphasis is on token-based authentication therefore, we will not be using a database. In another tutorial, I will revisit this topic to create a MERN Stack Web application using token-based authentication.

Side note…

I wrote this post from a particular perspective. At the time of this post, I was a teaching assistant for a coding bootcamp and noticed a lot of students struggling debugging their console errors. In this guide, we analyze and resolve common errors. With that said…

Let’s Get Started

The 3 most important tools we will be using for this project are the following:

bcrypt: A library to help you hash passwords.

json-webtoken: A JSON object that is defined as a safe way to represent a set of information between two parties.

dotenv: A zero-dependency module that loads environment variables from a .env file into process.env .

cookie-parser: Parse Cookie header and populate req.cookies with an object keyed by the cookie names. Optionally you may enable signed cookie support by passing a secret string, which assigns req.secret so it may be used by other middleware.

N ow, lets set up our app:

Assuming you have Node.js and Git installed, run the following commands in your terminal to get started.

In this tutorial config and models is not needed. However, if we were using a database the config directory would contain our database connection and our model's directory would contain the models of our data.

In your .env file, change the following:

The last change is to update our package.json file and add a script for nodemon. In package.json add the following:

Heres a summary of what we’ve just done.

  1. made a directory called authentication on your desktop and navigated to the directory
  2. we initialize the project with git
  3. we initialize the project with a package.json file with default settings
  4. we install our dependencies
  5. we create our empty server file and the needed directories
  6. created a Users.json file, that would contain our mock database of users
  7. created a file called .ignore and write names of files/folders to ignore
  8. create a file called .env that will hold the environment vairables that will be used throughout our project by using process.env .

Let's start the server.js file by running npm run nodemon from the terminal.

let's add some code to our express app. I will not explain in detail what each line of code does, however, the answer to these question can quickly be found with a bit of google foo! Moving on…

Congrats! We have our first error! ��

We’ll get to that soon enough. But before then, a few things to note. In our code, we are using destructuring:

Now, for the Error… ��

At first, this can be overwhelming, but if we look closely…

  1. What does our error say? Error: Cannot find module './routes/user'
  2. What does this error mean? Cannot find our module userRoutes
  3. Why are we getting this error? ur we are requiring a variable that references a file which does not exist yet
  4. How can we fix this error? Create a file called user.js inside of the routes directory

Side Note: what does app.use('/users', userRoutes) do?

app.use('/users', userRoutes) prefixes all of our routes inside the routes/user.js with /users .

What we are doing here is requiring & exporting the, and using our controller functions to handle our routes . These functions getUser, cookieCheck, login, logout, signup are used to do something on these particular routes. We will review these in a second. But first…

We now have another Error! ��

Take some time to think about how to answer the following questions:

  1. What does our error say?
  2. What does this error mean?
  3. Why is this error happening?
  4. How can we fix this error?

Answer: our controller functions are not defined.

To resolve this error, let's create a file called user.js inside of the controllers directory and insert the following code:

Oh look, another Error, yay!�� �� This is the last one I promise ��

How do we fix this one? no clue?

Let’s google it! �� …Wait, what do I google?

Answer: SyntaxError: Unexpected end of JSON input

…If you didn’t find the answer, here is why we get the error:

We do not have any data in our dbUsers.json file

How to Fix?

Open our config/dbUsers.json file and copy �� our mock data.

Make sure the data is properly formatted as JSON by using double quotes " "

For demonstration purposes, we’ve hardcoded plainTextPasswords and hashedPasswords for testing our mock users. However, we will create new users and hash their passwords.

Congratulations!

You’ve made it this far. At this point, it would be a good idea to start testing our routes and ensure everything is working correctly. If you have not done so already, go ahead and download POSTMAN.

��Before you start testing routes!!

Every time you save your JavaScript files nodemon will restart the application. Therefore, our newly created users are deleted every time we save.

�� Here’s what our routes look like, the corresponding methods, and the response:

�� Your application structure should look like this:

You can ignore the images directory and the README.md those were included in the image while testing the tutorial.

Now that all our routes are defined, let write the logic for hashing passwords and start testing using POSTMAN.

���� Testing our Routes in POSTMAN

Let’s open POSTMAN and make our first request.

TODO:

  1. set our HTTP METHOD to GET
  2. set our route name to http://localhost:PORT/users/all
  3. ensure the PORT is the correct number. Check your .env
  4. finally, click send

Notice on our GET /users/all route, our passwords are stored as plain text. What we need to do is store the plainTextPasswords as hashedPasswords using bcrypt.

Let's create a file called passwordService.js in our utility directory and insert the following code:

  1. What we are doing here is requiring the bcrypt package and using our environment variable SALTROUNDS from our .env file.
  2. Exporting two functions, hashPassword and checkPassword which will be used in our login and register routes. This would typically be done on your user model but because we are not using an official database we will use them directly on our routes.
  • hashPassword : is an async function that takes a plain text password as an argument , uses the bcrypt.hash method which takes a plain text as its first argument and the number of salt rounds as its second argument and returns the hashed password .
  • checkPassword : is an async function that takes a plain text password as its first argument and a hashed password as its second argument and uses the bcrypt.compare method which takes a plain text as its first argument and the hash as its second argument and returns the hashed password .

Although our environment variable for SALTROUNDS is a number, we still need to parse our value as an Integer.

SaltRounds…Ummm what's that? ��

SALTROUNDS: “The Cost Factor”

The cost factor controls the amount of time needed to calculate a single BCrypt hash. The higher the cost factor, the more hashing rounds is done. Increasing the cost factor by one doubles the necessary time. The higher the cost factor, the more difficult a hash is susceptible to brute-forcing.

  1. The salt is a random value and should differ for each calculation. The results should hardly ever be the same, even for equal passwords.
  2. The salt is usually included in the resulting hash-string in a readable form. So with storing the hash-string you also store the salt.
  3. A cost factor of 10 means that the calculation is done 2¹⁰ times which is about 1000 times. The more rounds of calculation you need to get the final hash, the more CPU/GPU time is necessary. This is no problem for calculating a single hash for a login, but it is a huge problem when you brute-force millions of password combinations.

We have our function, how do we use it?

For this exercise, we will modify each route individually, so be sure to always copy and paste the new constant variables located at the top of each code snippet.

Insert the following code for the signup route:

What we are doing here is requiring a package uuid which creates a unique id for our newUser object. Then we use our hashPassword middleware function and pass our password from the post in our req.body . We then push our newUser to our dbUsers.json file, send a status code to our client and redirect to our /users/all route.

Test your /users/signup route using POSTMAN

  • HTTP METHOD: POST ,
  • ROUTE: http://localhost:yourPortNumber/users/signup
  • click on body
  • select x-www-form-urlencoded
  • finally, add a key-value pair of username, password

Once you submit your POST you should have a new user in the response

Wallah! You should now be able to create a new user which should have an id, username, password, and a hashedPassword field.

Next, we will add functionality to our login route using our checkPassword middleware function from utilities/passwordService.js .

Add the following code to our login route:

Test your login route using POSTMAN

What we are doing in our login route is returning the first user in our “database” that matches the req.body.username. Then we are using our checkPassword function which returns a Boolean value . The check password function checks if the req.body.password matches the hashedPassword from our user .

Now we conditionally check if a user exists. If the user exists, we then check if the password matches. If all goes well, we send a 202 status code and redirect to our authorized route. Otherwise, we send errors to the client explaining what went wrong. In a real application, your message would look more something like: “sorry, the credentials you have entered are incorrect please try again”.

We now need to implement json-webtokens and ��’s.

Let's create and open a file utilities/tokenService.js . In this file, we need the jsonwebtoken package and our SECRET environment variable. We will then create two functions, createToken and isValidToken .

What we expect the code above to do is create a token by using jwt to sign a token. Passing in an object with our expected user, and an expiration time, and our SECRET environment variable. Then we return the token. Additionally, we verify our token using jwt.verify which takes an expected token string as its first argument and our SECRET environment variable.

Now in our controllers/user.js file lets refactor our code and each route to use our token service and cookies.

Updating our Route handlers, one by one

������MAKE SURE TO TEST EVERY ROUTE!!

In the above snippet:

  • require UUID which automatically generates unique id’s for our users.
  • require our Users mock database.
  • require our utilities for our password Service and token Service.
  • create an object called cookie options which will be explained below.

In the above snippet:

  • we create a getUsers handler that sends our Users array to the client.

In the above snippet, we create a login handler which:

  1. Finds a user by its username
  2. Checks the submitted password to our users hashedPassword
  3. Then we check if the user exists if true…
  4. We then check if the password matches, if true…
  5. Create a token with the user object.
  6. Create a cookie called token, pass in our token, and our cookieOptions and redirect to authorized route.

Notice, we console.log("token", token); . Copy �� this token from the terminal and visit JWT. Scroll down to the debugger and past your token in the encoded area.

Our token, when decoded contains a header, payload, and signature. Where the header contains the algorithm and type of token, the payload contains the data, and the signature verifies if our signature matches our SECRET environment variable.

Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication

Аутентификация(authentication, от греч. αὐθεντικός [authentikos] – реальный, подлинный; от αὐθέντης [authentes] – автор) — это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя путём сравнения введённого им логина/пароля с данными сохранёнными в базе данных.

Авторизация(authorization — разрешение, уполномочивание) — это проверка прав пользователя на доступ к определенным ресурсам.

Например, после аутентификации юзер sasha получает право обращаться и получать от ресурса «super.com/vip» некие данные. Во время обращения юзера sasha к ресурсу vip система авторизации проверит имеет ли право юзер обращаться к этому ресурсу (проще говоря переходить по неким разрешенным ссылкам)

  1. Юзер c емайлом sasha_gmail.com успешно прошел аутентификацию
  2. Сервер посмотрел в БД какая роль у юзера
  3. Сервер сгенерил юзеру токен с указанной ролью
  4. Юзер заходит на некий ресурс используя полученный токен
  5. Сервер смотрит на права(роль) юзера в токене и соответственно пропускает или отсекает запрос

Собственно п.5 и есть процесс авторизации.

Дабы не путаться с понятиями Authentication/Authorization можно использовать псевдонимы checkPassword/checkAccess(я так сделал в своей API)

JSON Web Token (JWT) — содержит три блока, разделенных точками: заголовок(header), набор полей (payload) и сигнатуру. Первые два блока представлены в JSON-формате и дополнительно закодированы в формат base64. Набор полей содержит произвольные пары имя/значения, притом стандарт JWT определяет несколько зарезервированных имен (iss, aud, exp и другие). Сигнатура может генерироваться при помощи и симметричных алгоритмов шифрования, и асимметричных. Кроме того, существует отдельный стандарт, отписывающий формат зашифрованного JWT-токена.

Пример подписанного JWT токена (после декодирования 1 и 2 блоков):

Токены предоставляют собой средство авторизации для каждого запроса от клиента к серверу. Токены(и соответственно сигнатура токена) генерируются на сервере основываясь на секретном ключе(который хранится на сервере) и payload’e. Токен в итоге хранится на клиенте и используется при необходимости авторизации какого-либо запроса. Такое решение отлично подходит при разработке SPA.

При попытке хакером подменить данные в header’ре или payload’е, токен станет не валидным, поскольку сигнатура не будет соответствовать изначальным значениям. А возможность сгенерировать новую сигнатуру у хакера отсутствует, поскольку секретный ключ для зашифровки лежит на сервере.

access token — используется для авторизации запросов и хранения дополнительной информации о пользователе (аля user_id, user_role или еще что либо, эту информацию также называет payload). Все поля в payload это свободный набор полей необходимый для реализации вашей частной бизнес логики. То бишь user_id и user_role не являются требованием и представляют собой исключительно частный случай. Сам токен храним не в localStorage как это обычно делают, а в памяти клиентского приложения.

refresh token — выдается сервером по результам успешной аутентификации и используется для получения новой пары access/refresh токенов. Храним исключительно в httpOnly куке.

Каждый токен имеет свой срок жизни, например access: 30 мин, refresh: 60 дней

Поскольку токены(а данном случае access) это не зашифрованная информация крайне не рекомендуется хранить в них какую либо sensitive data (passwords, payment credentials, etc. )

Роль рефреш токенов и зачем их хранить в БД. Рефреш на сервере хранится для учета доступа и инвалидации краденых токенов. Таким образом сервер наверняка знает о клиентах которым стоит доверять(кому позволено авторизоваться). Если не хранить рефреш токен в БД то велика вероятность того что токены будут бесконтрольно гулять по рукам злоумышленников. Для отслеживания которых нам придется заводить черный список и периодически чистить его от просроченных. В место этого мы храним лимитированный список белых токенов для каждого юзера отдельно и в случае кражи у нас уже есть механизм противодействия(описано ниже).

Как ставить куки ?

Для того что бы refreshToken кука была успешно уставленна и отправлена браузером, адреса эндпоинтов аутентификации( /api/auth/login , /api/auth/refresh-tokens , /api/auth/logout ) должны располагася в доменном пространстве сайта. Тоесть для домена super.com на сервере ставим куку с такими опциями:

Таким образом кука установится в браузер и прийдет на все эндпоинты по адресу super.com/api/auth/<any-path>

Если у нас монолит и за аутентификацию отвечает один и тот-же API, тут проблем не должно быть. Но если за аутентификацию отвечает отдельный микросервис, прячем его средствами nginx по выше указанному пути ( super.com/api/auth ).

Логин, создание сессии/токенов (api/auth/login):

  1. Пользователь логинится в приложении, передавая логин/пароль и fingerprint браузера (ну или некий иной уникальный идентификатор устройства если это не браузер)
  2. Сервер проверят подлинность логина/пароля
  3. В случае удачи создает и записывает сессию в БД < userId: uuid, refreshToken: uuid, expiresIn: int, fingerprint: string, . >(схема таблицы ниже)
  4. Создает access token
  5. Отправляет клиенту access и refresh token uuid (взятый из выше созданной сессии)
  1. Клиент сохраняет токены(access в памяти приложения, refresh сетится как кука автоматом)

На что нужно обратить внимание при установке refresh куки:

  • maxAge куки ставим равную expiresIn из выше созданной сессии
  • В path ставим корневой роут auth контроллера ( /api/auth ) это важно, таким образом токен получат только те хендлеры которым он нужен( /api/auth/logout и /api/auth/rerfesh-tokens ), остальные обойдутся(нечего зря почём отправлять sensitive data).

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

Таким образом если юзер залогинился на пяти устройствах, рефреш токены будут постоянно обновляться и все счастливы. Но если с аккаунтом юзера начнут производить подозрительные действия(попытаются залогинится более чем на 5’ти устройствах) система сбросит все сессии(рефреш токены) кроме последней.

Перед каждым запросом клиент предварительно проверяет время жизни access token’а (да берем expiresIn прямо из JWT в клиентском приложении) и если оно истекло шлет запрос на обновление токенов. Для большей уверенности можем обновлять токены на несколько секунд раньше. То есть кейс когда API получит истекший access токен практически исключен.

Что такое fingerprint ? Это инструмент отслеживания браузера вне зависимости от желания пользователя быть идентифицированным. Это хеш сгенерированный js’ом на базе неких уникальных параметров/компонентов браузера. Преимущество fingerprint’a в том что он нигде персистентно не хранится и генерируется только в момент логина и рефреша.

  • Библиотека для хеширования: https://github.com/Valve/fingerprintjs2
  • Более подробно: https://player.vimeo.com/video/151208427
  • Пример ф-ции получения такого хеша: https://gist.github.com/zmts/b26ba9a61aa0b93126fc6979e7338ca3

В случае если клиент не браузер, а мобильное приложение, в качестве fingerprint используем любую уникальную строку(тот же uuid ) персистентно хранящуюся на устройстве.

Рефреш токенов (api/auth/refresh-tokens):

Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я храню это список в PostgreSQL таблице(а надо бы в Redis’е). В процессе каждого логина создается запись с IP/Fingerprint и другой мета информацией, так званая рефреш-сессия.

  1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token’на
  2. Если истекло клиент делает запрос на POST auth/refresh-tokens < fingerprint: string >в body и соответственно refreshToken куку.
  3. Сервер получает запись рефреш-сессии по UUID’у рефреш токена
  4. Сохраняет текущую рефреш-сессию в переменную и удаляет ее из таблицы
  5. Проверяет текущую рефреш-сессию:
    1. Не истекло ли время жизни
    2. На соответствие старого fingerprint’a полученного из текущей рефреш-сессии с новым полученным из тела запроса

    Tip: Для отправки запроса с куками для axios есть опция

    Ключевой момент:

    В момент рефреша то есть обновления access token’a обновляются ОБА токена. Но как же refresh token может сам себя обновить, он ведь создается только после успешной аутентификации ? refresh token в момент рефреша сравнивает себя с тем refresh token’ом который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены.

    Вопрос зачем refresh token’y срок жизни, если он обновляется каждый раз при обновлении access token’a ? Это сделано на случай, если юзер будет в офлайне более 60 дней, тогда придется заново вбить логин/пароль.

    Logout (api/auth/logout)

    1. Front-end делает кол POST: api/auth/logout c refreshToken в куке или бади (лучше в куки)
    2. Front-end удаляет локально сохраненный в памяти accessToken
    3. Back-end удаляет запись из таблицы refreshSessions по refreshToken

    accessToken умирает по истечению строка его жизни. Руками банить, удалять, хранить accessToken не нужно, это нарушает всю суть эксесс токена.

    В случае кражи access токена и refresh куки:

    1. Хакер воспользовался access token’ом
    2. Закончилось время жизни access token’на
    3. Клиент хакера отправляет refresh token и fingerprint
    4. Сервер смотрит fingerprint хакера
    5. Сервер не находит fingerprint хакера в рефреш-сессии и удаляет ее из БД
    6. Сервер логирует попытку несанкционированного обновления токенов
    7. Сервер перенаправляет хакера на станицу логина. Хакер идет лесом
    8. Юзер пробует зайти на сервер >> обнаруживается что refresh token отсутствует
    9. Сервер перенаправляет юзера на форму аутентификации
    10. Юзер вводит логин/пароль

    В случае кражи access токена, refresh куки и fingerprint’а:

    Стащить все авторизационные данные это не из легких задач, но все же допустим этот кейс как крайний и наиболее неудобный с точки зрения UX (без примера в кодовой базе supra-api-nodejs ).

    Предложу несколько вариантов решения данной проблемы:

    • Хранить IP или Subnet залогиненного клиента
    1. Хакер воспользовался access token’ом
    2. Закончилось время жизни access token’на
    3. Хакер отправляет refresh куку и fingerprint
    4. Сервер проверяет IP хакера, хакер идет лесом

    UX минус: нужно логинится с каждого нового IP.

    • Удалять все сессии в случае если refresh токен не найден
    1. Хакер воспользовался access token’ом
    2. Закончилось время жизни access token’на
    3. Хакер отправляет refresh куку и fingerprint
    4. На сервере создается новый refresh токен («от хакера»)
    5. Хакер получает новую пару токенов
    6. Юзер пробует отправить запрос на сервер >> обнаруживается что refresh токен не валиден
    7. Сервер удаляет все сессии юзера, в последствии чего хакер больше не сможет обновлять access токен
    8. Сервер создает новую сессию для пользователя

    UX минус: в каждом случае когда сервер не будет находить рефреш токен — будут сбрасиватся все сессии юзера на всех устройствах.

    Зачем все это ? JWT vs Cookie sessions

    Зачем этот весь геморой ? Почему не юзать старые добрые cookie sessions ? Чем не угодили куки ?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *