Что такое JWT токен?

Что такое jwt?

Если обратиться с этим запросом в Google, вероятнее всего, он выдаст что-то подобное:

… способ представления передаваемых данных …… объект JSON, который определен в RFC 7519 как безопасный способ обмена информацией между двумя сторонами …… открытый стандарт, который определяет компактный и автономный способ безопасной передачи информации …

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

Веб-токен JSON, или JWT (произносится “jot”), представляет собой стандартизированный, в некоторых случаях подписанный и/или зашифрованный формат упаковки данных, который используется для безопасной передачи информации между двумя сторонами.

Проанализируем эту формулировку.

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

Аутентификация на основе токенов в последние годы стала очень популярна из-за распространения одностраничных приложений, веб-API и интернета вещей. Чаще всего в качестве токенов используются Json Web Tokens (JWT). Хотя реализации бывают разные, но токены JWT превратились в стандарт де-факто.

При аутентификации на основе токенов состояния не отслеживаются. Мы не будем хранить информацию о пользователе на сервере или в сессии и даже не будем хранить JWT, использованные для клиентов.

Процедура аутентификации на основе токенов:

Payload

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

  • iss — (issuer) издатель токена
  • sub — (subject) “тема”, назначение токена
  • aud — (audience) аудитория, получатели токена
  • exp — (expire time) срок действия токена
  • nbf — (not before) срок, до которого токен не действителен
  • iat — (issued at) время создания токена
  • jti — (JWT id) идентификатор токена

Все эти claims не являются обязательными, но их использование не по назначению может привести к коллизиям.

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

Signature

Подпись генерируется следующим образом: Закодированные заголовок и полезная нагрузка объединяются с точкой (“.”) в качестве разделителя. Затем эта строка хешируется указанным в header алгоритмом. Результат работы алгоритма хеширования и есть подпись.

В нашем примере токен будет выглядеть так: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBdXRoIFNlcnZlciIsInN1YiI6ImF1dGgiLCJleHAiOjE1MDU0Njc3NTY4NjksImlhdCI6MTUwNTQ2NzE1MjA2OSwidXNlciI6MX0.9VPGwNXYfXnNFWH3VsKwhFJ0MazwmNvjSSRZ1vf3ZUU

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

Уверен, эта картинка знакома всем:

Аутентификация или авторизация?

Некоторые путают термины «аутентификация» и «авторизация». Это разные вещи.

Ещё тут?

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

Аутентификация по 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 токена. После повторно отправляем изначальный запрос.

Беспарольная аутентификация

Первой реакцией на термин «беспарольная аутентификация» может быть «Как аутентифицировать кого-то без пароля? Разве такое возможно?»

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

Беспарольная аутентификация — это способ конфигурирования процедуры входа и аутентификации пользователей без ввода паролей. Идея такая:

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

Есть похожий метод, при котором вместо одноразовой ссылки по SMS отправляется код или одноразовый пароль. Но тогда придётся объединить ваше приложение с SMS-сервисом вроде twilio (и сервис не бесплатен). Код или одноразовый пароль тоже можно отправлять по почте.

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

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

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

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

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

refresh – 5 мин.

refresh мы сохраняем в cookie пользователя.

access держим в памяти, и при каждом обновлении страницы, выполняем запрос refresh для получения новой пары такенов. Либо есть вариант когда мы записываем access в cookie.

Не стоит хранить access или refresh токен в localstore, это не безопасно.

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

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

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

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

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

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

Двухфакторная аутентификация (2fa)

Двухфакторная аутентификация (2FA) улучшает безопасность доступа за счёт использования двух методов (также называемых факторами) проверки личности пользователя. Это разновидность многофакторной аутентификации. Наверное, вам не приходило в голову, но в банкоматах вы проходите двухфакторную аутентификацию: на вашей банковской карте должна быть записана правильная информация, и в дополнение к этому вы вводите PIN.

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

Десериализованная форма

В несериализованном виде JWT состоит из заголовка и полезной нагрузки, которые являются обычными JSON-объектами.

Ещё раз о безопасности или где хранить токен

git clone [email protected]:vporoshok/csrf-test.git
git checkout init
docker-compose up
my_1   | 172.19.0.1 - - [28/May... 0000] "GET /form.php HTTP/1.1" 401 426 "http://localhost:4001/" "Mozilla/.../603.1.30"
req.open('GET', 'http://localhost:4000/form.php');
req.withCredentials = true;
req.addEventListener('readystatechange', e => {

Защита веб-токенов

Как уже упоминалось выше, JWT использует два механизма для защиты информации: подписи и шифрование. Их описывают стандарты безопасности JSON Web Signature (JWS) и JSON Web Encryption (JWE).

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

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

Рассмотрим подобную атаку на нашем примере. Наш токен в незакодированном виде выглядит следующим образом

Использование библиотек

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

const jwt = require('jsonwebtoken');
const secret = 'shhhhh';
// шифрование
const token = jwt.sign({ foo: 'bar' }, secret);
// проверка и расшифровка
const decoded = jwt.verify(token, secret);
console.log(decoded.foo) // bar

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

Неподписанные json токены

Заголовок описывает криптографические операции, которые применяются к веб-токену. Но в некоторых случаях подпись и шифрование могут отсутствовать. Обычно это происходит, когда JWT является частью некоторой уже зашифрованной структуры данных. В заголовке такого неподписанного токена заявка alg должна быть равна none:

{
   "alg": "none"
}

Обновление токена

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

«expires_in»

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

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

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

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

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

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

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

Здесь рекомендации будут следующие:

  • как и в первом случае, использовать защищенное соединение при передаче токенов;
  • ограничить время жизни JWT и использовать механизм refresh tokens.

Подбор ключа симметричного алгоритма подписи

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

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

В нашем примере из первой части статьи для подписи JWT в качестве ключевой фразы была использована строка password. Она простая, короткая и содержится во всех основных словарях для перебора паролей. Злоумышленнику не составит труда подобрать эту ключевую фразу с использованием программ John the Ripper или hashcat.

Рекомендации для защиты от атаки в этом случае такие:

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

Подпись jwt

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

Самый распространенный алгоритм подписи – HMAC. Он объединяет полезную нагрузку с секретом, используя криптографическую хеш-функцию (чаще всего SHA-256). С помощью полученной уникальной подписи можно верифицировать данные. Это схема называется разделением секрета, поскольку он известен обеим сторонам: создателю и получателю. Таким образом, и тот, и другой могут генерировать новое подписанное сообщение.

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

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

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

Полезные данные – часть токена, в которой размещается вся необходимая пользовательская информация. Как и заголовок, полезная нагрузка представляет собой обычный объект JSON. Здесь ни одно поле не является обязательным. Обычно используются уже рассмотренные служебные заявки iss, sub и aud, а также специфические для приложения данные. Например, вот так выглядит JWT во фреймворке OpenID:

Получение токена

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

Хранить данные для авторизации мы будем в sessionStorage или localStorage, в зависимости от наших нужд. В первом случае данные хранятся до тех пор, пока пользователь не завершит сеанс или не закроет браузер, во втором случае данные в браузере будут храниться неограниченное время, пока по каким-либо причинам localStorage не будет очищен.

Преимущества jwt

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

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

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

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

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

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

Приложения

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

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

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

Пример имплементации:

Front-end:

Back-end:

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

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

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

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

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

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

Сериализованные json токены

JSON web token в сериализованной форме – это строка следующего формата:

[ Header ].[ Payload ].[ Signature ]

Заголовок (header) и полезная нагрузка (payload) присутствуют всегда, а вот подпись (signature) может отсутствовать.

Пример компактной формы:

eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1c2VyMTIzIiwicHJvZHVjdElkcyI6WzEsMl19.

Она получена из следующих данных:

Создание функции-обертки

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

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

Уязвимости jwt

В этом разделе будут рассмотрены основные атаки на JWT и даны рекомендации по их предотвращению.

Формат упаковки данных

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

Функция для обновления токена


function refreshToken(token) {
    return fetch('api/auth/refreshToken', {
        method: 'POST',
        credentials: 'include',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            token,
        }),
    })
        .then((res) => {
            if (res.status === 200) {
                const tokenData = res.json();
                saveToken(JSON.stringify(tokenData)); // сохраняем полученный обновленный токен в sessionStorage, с помощью функции, заданной ранее
                return Promise.resolve();
            }
            return Promise.reject();
        });
}

С помощью кода выше мы перезаписали токен в sessionStorage и теперь по новой можем отправлять запросы к api.

Функция для получения токена:


function getTokenData(login, password) {
    return fetch('api/auth', {
        method: 'POST',
        credentials: 'include',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            login,
            password,
        }),
    })
        .then((res) => {
            if (res.status === 200) {
                const tokenData = res.json();
                saveToken(JSON.stringify(tokenData)); // сохраняем полученный токен в sessionStorage, с помощью функции, заданной ранее
                return Promise.resolve()
            }
            return Promise.reject();
        });
}


Таким образом мы получили токен с полями

«access_token»«refresh_token»«expires_in»

и сохранили его в

sessionStorage

для дальнейшего использования.

Функция для сохранения токена в sessionstorage:

function saveToken(token) {
    sessionStorage.setItem('tokenData', JSON.stringify(token));
}

Функция-обертка


export async function fetchWithAuth(url, options) {
    
    const loginUrl = '/login'; // url страницы для авторизации
    let tokenData = null; // объявляем локальную переменную tokenData

    if (sessionStorage.authToken) { // если в sessionStorage присутствует tokenData, то берем её
        tokenData = JSON.parse(localStorage.tokenData);
    } else {
       return window.location.replace(loginUrl); // если токен отсутствует, то перенаправляем пользователя на страницу авторизации
    }

    if (!options.headers) { // если в запросе отсутствует headers, то задаем их
        options.headers = {};
    }
    
    if (tokenData) {
        if (Date.now() >= tokenData.expires_on * 1000) { // проверяем не истек ли срок жизни токена
            try {
                const newToken = await refreshToken(tokenData.refresh_token); // если истек, то обновляем токен с помощью refresh_token
                saveToken(newToken);
            } catch () { // если тут что-то пошло не так, то перенаправляем пользователя на страницу авторизации
               return  window.location.replace(loginUrl);
            }
        }

        options.headers.Authorization = `Bearer ${tokenData.token}`; // добавляем токен в headers запроса
    }

    return fetch(url, options); // возвращаем изначальную функцию, но уже с валидным токеном в headers
}

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


Просто достаточно будет «импортнуть» ее в файл и заменить на нее стандартный fetch.

import fetchWithAuth from './api';

function getData() {
    return fetchWithAuth('api/data', options)
}

Шифрование

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

Зашифрованный JWT известен как JWE (JSON Web Encryption). В отличие от JWS, его компактная форма имеет 5 сегментов, разделенных точкой. Дополнительно к зашифрованному заголовку и полезной нагрузке он включает в себя зашифрованный ключ, вектор инициализации и тег аутентификации.

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

Схема разделенного секрета аналогична механизму подписки. Все стороны знают секрет и могут шифровать и дешифровать токен.

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

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

Перевод статьи Max NgWizard K: A plain English introduction to JSON web tokens (JWT): what it is and what it isn’t.

Заключение

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

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

Похожее:  Упрощаем регистрацию и вход на сайт / Хабр

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

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