🕸 Веб-аутентификация: файлы cookies или токены?

Аутентификация на основе токенов

Этот способ был введен для устранения основанного на куках аутентификации. Особенности подхода – требуется ручная реализация и токены сохраняются на стороне клиента.

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

Стандартные реализации
токен-подхода в разы сложнее описанных выше. Например, в
OpenID Connect применяется несколько потоков аутентификации для различных сценариев использования. Чтобы лучше понять, как работают токены, разобьем процесс на четыре части и в качестве примера используем JWT (JSON Web Token) – наиболее широко используемый стандарт.

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

При передаче обычно необходимо
использовать шифрование (например, SSL) для защиты канала.

На стороне сервера можно
использовать библиотеку NPM (такую как jsonwebtoken) для
создания токенов:

Сервер отправляет файл cookie браузеру, включая его в заголовок Set-Cookie

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

Сохранение токена в хранилище браузера и добавление в запросы с помощью JavaScript

Браузер может хранить
этот маркер в локальном хранилище, хранилище сеансов или в хранилище cookies.
Затем он будет добавлен в заголовок авторизации и отправлен на сторону сервера
для валидации запросов.

Добавление токена в заголовок должно быть реализовано с помощью JavaScript.

Authorization: Bearer <token>

Кроме того, можно использовать
функцию jwt.decode() из библиотеки jsonwebtoken для декодирования токена.

Браузер сохраняет cookie в хранилище и отправляет его с последующими запросами

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

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

Когда пользователь выйдет из системы, сервер удалит сеанс из базы данных

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

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

Automatic logout

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

Best practices в jwt

И в завершении приведу best practices при использовании этого вида токенов, которых следует придерживаться:

Cookie-based authentication

Аутентификация – это процесс обмена учетными данными для идентификации пользователя. При аутентификации на основе cookies уникальный идентификатор (файл cookie) создается на стороне сервера и отправляется в браузер.

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

Чтобы лучше понять, как
работают файлы cookie, разобьем этот процесс на 5 частей.

Django rest framework

Теперь нужно установить Django REST Framework. Django REST Framework большой проект с открытым исходным кодом, для тех, кто хочет создавать одностраничные приложения и API для них.

Мы не будем детально рассматривать, как работает Django REST Framework, поэтому если вы не знакомы с ним, посмотрите документацию.

Для установки Django REST Framework, выполните следующую команду:

$ pip install djangorestframework


Django REST Framework должен быть добавлен к вашим установленным приложениям (INSTALLED_APPS в django_angular_token_auth/settings.py:)

INSTALLED_APPS = (
    ...,
    'rest_framework',
)

Так же необходимо добавить следующие параметры в django_angular_token_auth/settings.py:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ), 
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

Мы не будем использовать SessionAuthentication или BasicAuthentication, но они имеются в Django REST Framework и предлагают работоспособный API из коробки.

Django-rest-framework-jwt

Последнее, что нужно сделать, это установить django-rest-framework-jwt. Этот пакет обеспечивает поддержку JWT для Django REST Framework и в качестве JSON Web Token использует реализацию PyJWT. Что бы установить django-rest-framework-jwt выполните следующую команду:

$ pip install djangorestframework-jwt

Нужно будет добавить следующие параметры в django_angular_token_auth/settings.py:

import datetime
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=14)
}

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

Вы так же должны добавить обновленные настройки REST_FRAMEWORK:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
   )
}

Обратите внимание, что мы добавили:’rest_framework_jwt.authentication.JSONWebTokenAuthentication’, в ‘DEFAULT_AUTHENTICATION_CLASSES’.

Header

The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.

For example:

{
  "alg": "HS256",
  "typ": "JWT"
}

Then, this JSON is Base64Url encoded to form the first part of the JWT.

Json web tokens

Учитывая указанные проблемы, в сообществе разработчиков много лет назад появилась идея использовать некую строку, которую с одной стороны невозможно подделать, с другой — которую каждый ресурсный сервер мог бы сам проверить на валидность. В качестве такой строки оказалось удобно использовать JWT — JSON Web Token.

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

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

Но никто не может прочитать записанное в третьей части — и соответственно никто не может подделать токен.

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

Перейдем к следующей схеме на иллюстрации ниже. Схема похожа на аутентификацию с помощью сессий, но есть одно отличие. После введения кредов сервер дает юзеру не cookies с Session ID, а токен. Затем клиентское приложение добавляет токен к каждому запросу в виде специального заголовка Authorization.

При этом оно вписывает в него слово «Bearer» (предъявитель), а после пробела — сам токен. Также на схеме представлен Auth middleware. Это достаточно стандартная часть любого фреймворка, которая умеет проверять, в частности, валидность токена. То есть обработка на сервере проходит этап проверки.

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

Logout и only one active device

Давайте подробнее рассмотрим задачи авторизации Logout и Only one active device. Сами по себе токены задуманы как stateless, то есть они не предназначены для размещения на сервере. А идея заключается в том, чтобы предусмотреть возможность хранения какой-то информации о них на сервере.

Такие токены будут храниться в «черном списке», пока не истечет срок их использования. С другой стороны, можно создать на сервере whitelist, что может оказаться даже несколько проще. Этот список станет реестром выданных токенов, с которым может сверяться система. Если предъявленный юзером токен не просрочен и указан в «белом списке», то он валиден.

Хранение самих токенов в базе данных сложно назвать безопасным. Можно ли как-то выйти из этой  ситуации? Да. Обратите внимание на иллюстрацию ниже: в разделе payload добавился пункт hash. Это некая рандомно сформированная строка. Она записывается в payload токена и в хранилище на сервере с привязкой к идентификатору пользователя.

Когда юзер предъявляет токен, система парсит его, проверяет срок действия токена и его подпись секретной строкой. Затем вытаскивает hash и сверяет его с пользовательским ID в хранилище. Это может быть key-value хранилище, основная база данных или другое хранилище, которое предоставит современный фреймворк.

Payload или полезные данные

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

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

Signature

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.

For example if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:

HMACSHA256(
  base64UrlEncode(header)   "."  
  base64UrlEncode(payload),
  secret)

The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.

Url-адреса

Теперь нужно настроить URL-адреса для нашего проекта.

Откройте django_angular_token_auth/urls.py и приведите его к следующему виду:

What is json web token?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties.

What is the json web token structure?

In its compact form, JSON Web Tokens consist of three parts separated by dots (.), which are:

Therefore, a JWT typically looks like the following.

xxxxx.yyyyy.zzzzz

Let’s break down the different parts.

When should you use json web tokens?

Here are some scenarios where JSON Web Tokens are useful:

Why should we use json web tokens?

Let’s talk about the benefits of JSON Web Tokens (JWT) when compared to Simple Web Tokens (SWT) and Security Assertion Markup Language Tokens (SAML).

Безопасность

Cookie не имеют надежной защиты от атак, и они в основном уязвимы для атак с использованием межсайтового скриптинга (XSS) и подделки межсайтовых запросов (CSRF).

Мы можем явно изменить заголовки файлов cookie, чтобы защитить их от таких атак.

Во второй части

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

Заголовок

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

Тип маркера хранится в ключе «typ». Ключ «typ» игнорируется в JWT (для этой статьи это не важно, но вы можете

, чтобы узнать почему). Если ключ «typ» присутствует, его значение должно быть JWT, чтобы указать, что этот объект является JSON Web Token.

Второй ключ «alg» определяет алгоритм, используемый для шифрования маркера. По умолчанию он должен быть установлен в HS256. Есть целый ряд альтернативных алгоритмов, которые используются в различных реализациях. Полный список с разбивкой по библиотекам можно посмотреть на vhod-v-lichnyj-kabinet.ru.

Заголовок кодируется в base64.

Зачем 2 токена?

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

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

Использование алгоритма хеширования none

Подобная атака происходит, когда злоумышленник изменяет токен, а также меняет алгоритм хеширования (поле “alg”), чтобы указать через ключевое слово none, что целостность токена уже проверена. Некоторые библиотеки рассматривали токены, подписанные с помощью алгоритма none, как действительный токен с проверенной подписью, поэтому злоумышленник мог изменить полезную нагрузку (payload) токена, и приложение доверяло бы токену.

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

Пример реализации:

// Ключ HMAC хранится как String в памяти JVM
private transient byte[] keyHMAC = ...;

...

// Создание контекста верификации для запроса к токену
// Явно указывается использование HMAC-256 хеш-алгоритма
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build();

// Верификация токена
DecodedJWT decodedToken = verifier.verify(token);

Использование слабого ключа при создании токена

Если секрет, используемый в случае алгоритма HMAC-SHA256, необходимый для подписи токена, является слабым, то он может быть взломан (подобран c помощью атаки грубой силы). В результате злоумышленник может подделать произвольный действительный токен с точки зрения подписи.

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

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

Например:

A&'/}Z57M(2hNg=;LE?~]YtRMS5(yZ<vcZTA3N-($>2j:ZeX-BGftaVk`)jKP~q?,jk)EMbgt*kW'

Для оценки сложности секретного ключа, используемого для вашей подписи токена, вы можете применить атаку по словарю паролей к токену в сочетании с JWT API.

Могут возникнуть проблемы с масштабируемостью

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

Хотя существуют хорошо
зарекомендовавшие себя способы управления масштабируемостью (например,
использование для хранения сеансов СУБД наподобие Redis), это все равно
добавляет сложности. По мере роста количества пользователей, могут возникнуть
проблемы с масштабированием и управлением сеансами.

Не подходит для api

Если вы создаете API для предоставления услуг клиентам, cookie – это не лучший вариант. Если клиент не является браузером, это усложнит работу.

Например, если вы
разрабатываете мобильное приложение, наличие файлов cookie усложнит управление
файлами по сравнению с токеном.

Обычно работают в одном домене

Файлы cookie работают только в одном домене, если вы специально их не настроили.

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

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

Пара слов о безопасности данных

Теперь, когда вы разобрались в устройстве и работе JSON веб-токенов, возникает естественный вопрос – защищены ли передаваемые данные?

Мы видели, что токен кодируется и подписывается, но – внимание! – не шифруется! (в чем разница между кодированием и шифрованием?) То есть эта обработка не обеспечивает безопасность данных. Нет никакой гарантии сохранности, поэтому не следует передавать в токенах что-то конфиденциальное.

Перехват токенов

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

Защита заключается в добавлении «пользовательского контекста» в токен. Пользовательский контекст будет состоять из следующей информации:

Подписываемся под данными

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

Схема генерации HMAC (hash-based message authentication code), кода аутентификации сообщений с использованием хеш-функции
Схема генерации HMAC (hash-based message authentication code), кода аутентификации сообщений с использованием хеш-функции

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

Подпись

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

Берутся закодированные в base64: заголовок и полезная нагрузка, они объединяются в строку через точку. Затем эта строка и секретный ключ поступает на вход алгоритма шифрования, указанного в заголовке (ключ «alg»). Ключом может быть любая строка. Более длинные строки будут наиболее предпочтительнее, поскольку потребуется больше времени на подбор.

Подходит для хранения дополнительных данных

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

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

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

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

Полезная нагрузка

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

{
    "email": "[email protected]"
}
 

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

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

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

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


Как и заголовок, полезная нагрузка кодируется в base64.

Постскриптум

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

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

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

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

// приводим к целочисленному числу uint32. Итого 32 бита.
// расчет простой: год 12 бит, месяц 4 бита, день 5 бит и т.д. Таким образом в аккурат умещаемся в 32 бита или 4 байта.
date := uint32(year<<20) | uint32(month<<16) | uint32(day<<11) | uint32(hour<<6) | uint32(minute)

// в цикле кодируем байты в ASCII. 1 знак это шесть бит. Итого и получаем шесть знаков даты по таблице ASCII – печатные знаки.
for n := 0; n < 6; n {
b6Bit = byte(date>>i) & 0x3F
sBuilder.WriteByte(byte8bitToASCII(b6Bit))

}

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

Представления


С сериализаторами разобрались, давайте теперь перейдем к представлениям.

Для наших целей нужно только одно представление, которое будет возвращать список объектов пользователя.

Django REST Framework предлагает целый ряд представлений, которые выполняют различные функции, например представление списка объектов одного типа. Такую функциональность предлагает класс ListAPIView.


Добавьте следующее в authentication/views.py:

Придумываем коды доступа

Люди, которые придумали двухфакторную аутентификацию, по всей видимости, руководствовались принципом «одна голова хорошо, а две — лучше». И действительно — два пароля точно безопаснее одного. Но пароли, которые отправляет сайт в SMS, нельзя назвать абсолютно защищенными: сообщение чаще всего можно перехватить.

Создание безопасных одноразовых паролей состоит из двух этапов:

  1. Первичная настройка — включение двухфакторной аутентификации.
  2. Использование пароля — непосредственный ввод кода и отправка для проверки.

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

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

На самом деле весь секрет — последовательность из случайных символов, которые закодированы в формате Base32. Суммарно они занимают не меньше 128 бит, а чаще и все 190 бит. Эту последовательность и видит пользователь как текст или QR-код.

Так выглядит код QR для обмена секретом
Так выглядит код QR для обмена секретом

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

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

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

Возьмем время празднования Нового года в формате UNIX (1577811600000) и посчитаем порядковый номер нашего пароля: поделим на 30 секунд — 52593720. Воспользуемся нашим секретом и вычислим хеш — по стандарту RFC 6238 это функция SHA-1:

Проблемы безопасности

Хотя введением токенов
пытаются решить проблемы безопасности аутентификации на основе cookies, полностью от них не избавиться.

Сохраненные в браузере токены могут быть уязвимы для атак XSS, если приложение позволяет внедрять внешние сценарии JavaScript.

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

Проблемы использования токенов

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

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

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

Проверка токена

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

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

Различные типовые задачи с использованием jwt

Сегодня в проектах приходится решать задачи, которые выходят за рамки описываемых токенов, но при этом они базируются на их использовании. Первая подобная задача — lockout-механизм. Он предполагает блокирование пользователя после нескольких безуспешных попыток аутентификации.

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

Более интересные задачи — Logout и Only one active device, которые в принципе связаны между собой.

Logout предполагает, что после нажатия кнопки логаута во фронтенд-приложении надо с подтверждением выхода еще и инвалидировать сами токены, сделать их недействительными.

Раскрытие информации о токене

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

Способ защиты достаточно очевиден и заключается в шифровании токена. Также важно защитить зашифрованные данные от атак с использованием криптоанализа. Для достижения всех этих целей используется алгоритм AES-GCM, который обеспечивает аутентифицированное шифрование с ассоциированными данными (Authenticated Encryption with Associated Data – AEAD).

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

То есть шифрование с соответствующими данными обеспечивает подлинность и целостность данных, но не их секретность.

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

Реализация проверки подлинности с помощью json web token

Давайте пойдем дальше и создадим наш проект.

Перейдите в каталог проекта и выполните команды:

Сериализаторы


Для проверки работоспособности API нужно отправлять какие-нибудь данные на AJAX запросы. Самый простой способ это сделать — отправить обратно данные пользователя в нашей системе.

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

Сигнатура

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

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

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

Создание django приложения

Теперь давайте создадим приложение Django для хранения в нем представлений и сериализаторов:

$ python manage.py startapp authentication

Добавим ‘authentication’ в наш INSTALLED_APPS (в django_angular_token_auth/settings.py like so):

INSTALLED_APPS = (
	...,
	'rest_framework',
	'authentication',
)


Как привило после создания приложения нужно выполнить миграции, для создания моделей в БД. Но мы не будем создавать собственные модели, поэтому об этом не нужно беспокоиться.

Соображения по поводу использования jwt

Даже если токен JWT прост в использовании и позволяет предоставлять сервисы (в основном REST) без сохранения состояния (stateless), такое решение подходит не для всех приложений, потому что оно поставляется с некоторыми оговорками, как, например, вопрос хранения токена.

Если приложение не должно быть полностью stateless, то можно рассмотреть возможность использования традиционной системы сессий, предоставляемой всеми веб-платформами. Однако для stateless приложений JWT – это хороший вариант, если он правильно реализован.

Сравнение сессий и jwt

Наверняка кто-то скажет: «Ура, вы переоткрыли механизм сессий», и в этом есть доля правды. Токен нужен для подтверждения аутентификации пользователя. А идентификатор сесии — это признак пользовательской сессии: находится ли сейчас юзер в системе, может ли он работать с ней, или сессия завершена.

В интернете вам может попасться такая немного саркастическая картинка:

Хранение токенов на стороне клиента

Если приложение хранит токен так, что возникает одна или несколько из следующих ситуаций:

Для предотвращения атаки:

  1. Хранить токен в браузере, используя контейнер sessionStorage.
  2. Добавить его в заголовок Authorization, используя схему Bearer. Заголовок должен выглядеть следующим образом:
    Authorization: Bearer <token>
  3. Добавить fingerprint информацию к токену.

Сохраняя токен в контейнере sessionStorage, он предоставляет токен для кражи в случае XSS. Однако fingerprint, добавленный в токен, предотвращает повторное использование украденного токена злоумышленником на его компьютере. Чтобы закрыть максимум областей использования для злоумышленника, добавьте Политику безопасности содержимого браузера (Content Security Policy), чтобы ограничить контекст выполнения.

Остается случай, когда злоумышленник использует контекст просмотра пользователя в качестве прокси-сервера, чтобы использовать целевое приложение через легитимного пользователя, но Content Security Policy может предотвратить связь с непредвиденными доменами.

Также возможно реализовать службу аутентификации таким образом, чтобы токен выдавался внутри защищенного файла cookie, но в этом случае должна быть реализована защита от CSRF.

Шаблоны

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


Создайте файл с именем templates/index.html и добавьте следующий код в него:

В дальнейшем мы добавим некоторый angular код в данный файл.

Шаг 2. полезная нагрузка

Во втором компоненте JSON веб-токенов (payload) хранится информация о пользователе, которую сервер аутентификации передает серверу приложения. Стандарт предусматривает несколько необязательных для заполнения служебных полей, например:

  • exp – срок окончания действия токена;
  • nbf – время начала действия токена;
  • sub – уникальный идентификатор пользователя.

Если интересно, почитайте более подробное описание компонента полезной нагрузки в Википедии.

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

Шаг 3. подпись

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

data = base64urlEncode(header)   "."   base64urlEncode(payload)
hashedData = hash(data, secret)
signature = base64urlEncode(hashedData)

Разберем по шагам:

// header
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

// payload
eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ
// signature
-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

Шаг 4. cборка jwt

У нас уже есть все необходимые блоки для создания токена:

  • заголовок с указанием алгоритма хеширования;
  • полезная нагрузка с пользовательскими данными;
  • сгенерированная подпись.

Теперь можно собрать из них полноценный веб-токен по уже знакомой схеме:

header.payload.signature

Заголовок и полезная нагрузка предварительно кодируются в Base64URL, а подпись уже в правильном формате:

// JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1nij9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhltq4zjitogzhyi1jzwyzota0njywymqifq.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

Этот токен пользователь получит от сервера аутентификации и будет использовать при запросах к серверу приложения.

Это stateless-механизм

В отличие от cookie-подхода, вариант с токенами не имеет состояния. Это означает, что он не сохраняет никакой
информации о пользователях в базе данных или на сервере. Сервер отвечает только за
создание и проверку токенов, что позволяет реализовывать более масштабируемые
решения.

Это полностью автоматизированный процесс

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

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

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

Явное аннулирование токена пользователем

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

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

В черном списке будет храниться сборник (в кодировке SHA-256 в HEX) токена с датой аннулирования, которая должна превышать срок действия выданного токена.

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

Пример реализации:

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

create table if not exists revoked_token(jwt_token_digest varchar(255) primary key,
revokation_date timestamp default now());

Управление аннулированиями токенов:

// Контролирование отката токена (logout).
// Используйте БД, чтобы разрешить нескольким экземплярам проверять
// отозванный токен и разрешить очистку на уровне централизованной БД.
public class TokenRevoker {

 // Подключение к БД
 @Resource("jdbc/storeDS")
 private DataSource storeDS;

 // Проверка является ли токен отозванным
 public boolean isTokenRevoked(String jwtInHex) throws Exception {
     boolean tokenIsPresent = false;
     if (jwtInHex != null && !jwtInHex.trim().isEmpty()) {
         // Декодирование токена
         byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex);

         // Вычисление SHA256 от токена
         MessageDigest digest = MessageDigest.getInstance("SHA-256");
         byte[] cipheredTokenDigest = digest.digest(cipheredToken);
         String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest);

         // Поиск токена в БД
         try (Connection con = this.storeDS.getConnection()) {
             String query = "select jwt_token_digest from revoked_token where jwt_token_digest = ?";
             try (PreparedStatement pStatement = con.prepareStatement(query)) {
                 pStatement.setString(1, jwtTokenDigestInHex);
                 try (ResultSet rSet = pStatement.executeQuery()) {
                     tokenIsPresent = rSet.next();
                 }
             }
         }
     }

     return tokenIsPresent;
 }

// Добавление закодированного в HEX токена в таблица отозванных токенов
public void revokeToken(String jwtInHex) throws Exception {
     if (jwtInHex != null && !jwtInHex.trim().isEmpty()) {
         // Декодирование токена
         byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex);

         // Вычисление SHA256 от токена
         MessageDigest digest = MessageDigest.getInstance("SHA-256");
         byte[] cipheredTokenDigest = digest.digest(cipheredToken);
         String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest);

         // Проверка на наличие токена уже в БД и занесение в БД в
	   // обратном случае
         if (!this.isTokenRevoked(jwtInHex)) {
             try (Connection con = this.storeDS.getConnection()) {
                 String query = "insert into revoked_token(jwt_token_digest) values(?)";
                 int insertedRecordCount;
                 try (PreparedStatement pStatement = con.prepareStatement(query)) {
                     pStatement.setString(1, jwtTokenDigestInHex);
                     insertedRecordCount = pStatement.executeUpdate();
                 }
                 if (insertedRecordCount != 1) {
                     throw new IllegalStateException("Number of inserted record is invalid,"   " 1 expected but is "   insertedRecordCount);
                 }
             }
         }

     }
 }

Заключение

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

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

Спасибо за внимание!

***

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

Похожее:  Лучшие практики: Авторизация в мобильных приложениях — Офтоп на

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

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