Для начала разберем небольшую задачу. Она поможет читателю получить представление об основах шифрования.

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

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

Вариант 1

  1. В пункте А вешаем замок и отправляем в пункт Б.

  2. В пункте Б вешаем еще один замок и отправляем обратно в пункт А.

  3. В пункте А открываем замок который изначально повешали в пункте А  и возвращаем в пункт Б.

  4. В пункте Б открываем второй замок, извлекаем содержимое.

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

Избегаем лишних «движений»

Вариант 2

  1. Из пункта А отправляем замок без ключа.

  2. В пункте Б у нас есть замок и два комплекта ключей. Один из них положим в сундук и закроем на замок, который получили из пункта  А. Затем возвращаем обратно в пункт А.

  3. Получаем сундук в пункте А, открываем замок и достаем ключ.

Готово! Теперь в пунктах А и Б есть одинаковые ключи, и можно обмениваться данными, используя один замок.

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


Шифрование

Выделяют 2 типа шифрования: симметричное и асимметричное.

Симметричное шифрование - способ шифрования, в котором для шифрования и дешифрования применяется один и тот же криптографический ключ. Проблемой данного способа является передача ключа. Необходимо исключить возможность кражи при передаче.

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

Вспомним «Вариант 2» задачи в начале статьи. 

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

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

В пункте А откроем сундук ключом (расшифруем данные приватным ключом).

Теперь в пунктах А и Б есть одинаковые ключи, и можно проводить симметричное шифрование, использовать 1 замок и дубликаты ключей с каждой стороны.


Как работает SSL 

Когда пользователь заходит на сайт, устанавливается защищенное ssl соединение. Данная операция будет происходить каждый раз при новом заходе на сайт. При запросе браузер проверяет наличие сертификата у сервера. В ответ сервером отправляется публичный ключ с сертификатом. Браузер осуществляет проверку сертификата в центре сертификации. Если с сертификатом все хорошо генерирует сеансовый ключ, необходимый для симметричного шифрования и для обеспечения сервера копией данного ключа в дальнейшем. Когда сеансовый ключ сгенерирован, браузер зашифровывает его публичным ключом, полученным с сервера, затем отправляет обратно. Сервер расшифровывает сообщение приватным ключом и сохраняет сеансовый. Далее обмен данными происходит с помощью симметричного шифрования с использованием HTTPS.

JWT Описание стандарта. Правила хранения токенов. Способы передачи и проверки подлинности. Роль SSL при передаче данных.

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

JSON Web Token (JWT) — содержит три блока, разделенных точками

token =  { header.payload.signature }

header — заголовок,

payload — полезная нагрузка,

signature — подпись.

token = { alg:"HS256", typ:"JWT" }.{ iss:"auth.myservice.com", user:"test", role:"admin" }.eae1d799c0f6211da133ad35213

Header — заголовок

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

Header - служебная часть токена. Определяет как обрабатывать полученную информацию

typ — тип токена, например JWT;

alg — алгоритм, использованный для генерации подписи.

Payload — полезная нагрузка

{ iss:"auth.myservice.com", user:"John Smith", role:"Admin" }

Payload - Любые данные о пользователе, для дальнейшей идентификации

Список стандартных данных для payload:

iss (issuer) — определяет приложение, из которого отправляется токен.

sub (subject) — определяет тему токена.

exp (expiration time) — время жизни токена.

iss — (issuer) издатель токена.

sub — (subject) "тема", назначение токена.

aud — (audience) аудитория, получатели токена.

exp — (expire time) срок действия токена.

nbf — (not before) срок, до которого токен не действителен.

iat — (issued at) время создания токена.

jti — (JWT id) идентификатор токена.

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

Signature — подпись.

S9Zs/8/uEGGTVVtLggFTizCsMtwOJnRhjaQ2BMUQhcY

Signature - сумма header + payload, зашифрованных определенным образом.

header и payload кодируются при помощи алгоритма base64url, затем объединяются 

в единую строку с использованием точки (".") и шифруются алгоритмом с использованием секретного ключа.

signature = HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),SECRET_KEY)

Собранный токен

JWT = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + signature

Проверка JWT  токена на подлинность

получаем токен { header.payload.signature }

Создаем сигнатуру из header и payload 

newSignature = HMACSHA256( header + "." + payload, SECRET_KEY)

сравниваем получившиеся сигнатуру с сигнатурой из токена:

if(newSignature  === signature) { токен валидный и не был подделан }


Аутентификация по JWT

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

Access token -  для авторизации и идентификации пользователя.

Refresh token - для обновления access token.

Access token ограничен по времени жизни (например, 10 минут). Refresh token действителен дольше, например, месяц или неделю. Refresh token необходим для обновления access token. При истечении срока refresh token пользователь заново проходит процедуру аутентификации.

После получения токенов при последующих обращениях access токен передается приложению в заголовке запроса от пользователя

Получив токен от пользователя, приложение проверит его подпись и актуальность. Убедившись в действительности и не истекшем сроке жизни токена, приложение авторизует его используя данные из токена. Если на одном из этапов произошла ошибка, приложение вернет ошибку с кодом «401».

При получении ошибки «401» пользователю необходимо обновить access с помощью refresh токена, отправив запрос по api. Например, api/refresh. В ответ  получаем новую пару access и refresh токена. После повторно отправляем изначальный запрос.


Где хранить токены

Представим ситуацию: пользователь авторизовался и получил 2 токена:

access - живет 3 мин;

refresh - 5 мин.

Далее эти данные мы сохраняем в cookie пользователя. Не стоит хранить access или refresh токен в localstore.

Устанавливаем заголовки для set-cookie:

httpOnly: true, запрет чтения/записи данных Cookie посредством JavaScript

sameSite: true, SameSite=strict (для предотвращения CSRF-атак)

secure: true, разрешает обмен только по https (ssl) и предотвращает перехват access токена

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

Authorization = `Bearer ${access-token}`

Внимание! Не отправляйте refresh токен в cookie на сервер

Https (SSL) вас защитит, так как данные передаются в шифрованном виде, но данный подход не совсем верный, так как при изменении https на http появляется уязвимость. Вдобавок использование двух токенов теряет смысл.

При получении и записи cookie с refresh токеном у нее должен быть указать path равный url, по которому мы обновляем токены ‘api/refresh’. Так мы исключаем отправку refresh токена в других запросах. Это защищает от перехвата обоих токенов и бесконечного обновления access. 

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


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

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

Для предотвращения угроз рекомендуется:

  1. использовать защищенное соединение при передаче токенов;

  1. не использовать в токене секретную информацию о пользователе;

  1. для передачи токенов использовать https (ssl);

  1. использовать ограниченное время жизни токенов;

  1. не передавайте access и refresh токены вместе. При хранении токенов в cookie, нужно вырезать рефреш при отправке.

Если у злоумышленника окажется оба токена: refresh и access, он сможет выполнять refresh и обновлять их бесконечно. Наш аccess станет недействительным, и придется сделать logout. В противном случае это сделает приложение. После выполняется login, происходит получение новой пары refresh и access. Злоумышленник больше не сможет использовать токены. Это одно из преимуществ данной технологии.


Подбор ключа

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

Рекомендации для защиты от атаки подбора ключей:

  1. SECRET_KEY - рекомендуется хранить в env переменных;

  1. Использовать ключ большой длины: большие и малые буквы, цифры и спецсимволы;

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


Главная проблема LocalStorage

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

Пример инъекции в js

Допустим, у нас на сайте есть скрипт

<script src="https://lib.com/minified.js"></script>

Скрипт является взломанным и несет в себе вредоносный код, который прочитывает все данные в локальном хранилище и отправляет на сервер для сбора украденной информации. Используя внешние скрипты на сайте, мы всегда рискуем. То же самое касается и установки пакетов через npm или yarn: нет уверенности в их безопасности. Даже сам автор скрипта/плагина может использовать вредоносный код.

Альтернативы локальному хранилищу

Куки

Установка флага:

httpOnly - запретит js читать данные из cookie.

SameSite=strict для предотвращения CSRF-атак, а также флаг.

Secure=true - передача только по https

Сессии

Используйте серверную сессию для хранения секретных данных.


Заключение

SSL - стандарт нашего времени. Браузеры запрещают посещение ресурсов без этого протокола.

JSON Web Tokens — популярная технология. При правильном использовании JWT избавляет от ошибки недостаточной авторизации, позволяет быстро распределять информационные потоки между сервисами, с едиными учетными данными и повысить их производительность.

Комментарии (6)


  1. ggo
    04.03.2022 10:42

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

    Когда auth-сервис не наш, но мы ему доверяем, там понятно, для чего приседания с refresh- и access-токенами.


    1. amakhrov
      04.03.2022 11:44

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


  1. amakhrov
    04.03.2022 11:29

    Похоже на довольно сумбурный конспект лекции. Причем сразу на 2 отдельные темы: JWT и SSL.

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

    Authorization = `Bearer ${access-token}`

    Как же так? Чуть выше было сказано, что токен храним в http-only cookie.

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

    Не расшифровать, а декодировать. Ничего страшного в этом нет. Вы же не храните там важные данные в открытом виде, правда?

    Ну и там еще кой-чего по мелочи


    1. jtape Автор
      05.03.2022 12:43

      Большое спасибо за комментарий, все по делу. Моя первая статья)

      "Не расшифровать, а декодировать" - исправил

      "Как же так? Чуть выше было сказано, что токен храним в http-only cookie" - тут действительно недоработка с моей стороны,

      вижу следующие варианты:

      1) access-token храним без http-only, и для записи в header запроса используем js, но тогда создаем уязвимость, что не очень.

      2) вовсе не использовать заголовок для передачи access-token, оставляем его cookie

      что скажете по поводу 2 варианта?


      1. amakhrov
        06.03.2022 09:49

        Если и auth-провайдер, и сервисы - наши (см ветку выше), то нам refresh token на клиенте не нужен. а access token живет в httpOnly cookie, js-код на клиенте его не будет читать, да и вообще знать про него. С точки зрения клиента этот сценарий не отличается от классического session id в кукиз.

        Если auth-provider чужой, то refresh-токен храним в кукиз на домене этого провайдера (тоже httpOnly), запрашиваем у него access token и используем в заголовке запросов к сервисам. Этот access token просто держим в памяти, после обновления страницы заново ходим к auth-provider за новым токеном.

        Во втором варианте кукиз с refresh-токеном должна быть доступна для кросс-доменного запроса (заголовок Access-Control-Allow-Credentials: true, а сама кука SameSite=None;Secure). Если это нежелательно, то чуть усложняем. Чтобы получить первый refresh-токен, надо сделать редирект на сайт auth-провайдера. При получении access-токена (и нового refresh-токена) передаем refresh-токен в заголовке запроса к auth-провайдеру. Refresh-токен держим в памяти, никуда не записываем для безопасности. Поэтому такой редирект надо будет делать при каждом обновлении страницы.

        Можно придумать и более сложные сценарии, наверное. Но пока незачем.


  1. muturgan
    06.03.2022 09:59

    Не понял как при авторизации предполагается сделать set-cookie и передать на клиент сразу access и refresh токен.

    На сколько я помню, за один раз в заголовке set-cookie можно передать только 1 значение.

    Или предполагается делать 2 отдельных запроса к серверу для получения этих токенов?